diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 31e0a8be8ba5..8b17af4b00e2 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -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 diff --git a/.teamcity/OpenSourceProjects_Storybook/buildTypes/OpenSourceProjects_Storybook_Apps.kt b/.teamcity/OpenSourceProjects_Storybook/buildTypes/StotybookApp.kt similarity index 98% rename from .teamcity/OpenSourceProjects_Storybook/buildTypes/OpenSourceProjects_Storybook_Apps.kt rename to .teamcity/OpenSourceProjects_Storybook/buildTypes/StotybookApp.kt index c631162d7125..7696e11001cd 100644 --- a/.teamcity/OpenSourceProjects_Storybook/buildTypes/OpenSourceProjects_Storybook_Apps.kt +++ b/.teamcity/OpenSourceProjects_Storybook/buildTypes/StotybookApp.kt @@ -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() diff --git a/ADDONS_SUPPORT.md b/ADDONS_SUPPORT.md index 148b756ac9f0..3800de4b90ce 100644 --- a/ADDONS_SUPPORT.md +++ b/ADDONS_SUPPORT.md @@ -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)| -| ----------- |:-------:|:-------:|:-------:|:-------:|:-------:|:-------:| -|[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)| +| ----------- |:-------:|:-------:|:-------:|:-------:|:-------:|:-------:|:-------:| +|[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) |+| |+|+|+|+|+| diff --git a/README.md b/README.md index ff734ac1874f..27f0085e0c43 100644 --- a/README.md +++ b/README.md @@ -72,8 +72,9 @@ 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) release candidate +- [Polymer](app/polymer) - [Mithril](app/mithril) alpha +- [HTML](app/html) alpha ### Sub Projects @@ -84,7 +85,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 @@ -110,6 +111,7 @@ See [Addon / Framework Support Table](ADDONS_SUPPORT.md) - [Angular](https://storybooks-angular.netlify.com/) - [Polymer](https://storybooks-polymer.netlify.com/) - [Mithril](https://storybooks-mithril.netlify.com/) +- [HTML](https://storybooks-html.netlify.com/) ### 3.4 - [React Official](https://release-3-4--storybooks-official.netlify.com) diff --git a/addons/a11y/html.js b/addons/a11y/html.js new file mode 100644 index 000000000000..4f7edc6bedba --- /dev/null +++ b/addons/a11y/html.js @@ -0,0 +1 @@ +module.exports = require('./dist/html'); diff --git a/addons/a11y/package.json b/addons/a11y/package.json index 48d63b5dc08f..da339113437b 100644 --- a/addons/a11y/package.json +++ b/addons/a11y/package.json @@ -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": { diff --git a/addons/a11y/src/components/Panel.js b/addons/a11y/src/components/Panel.js index 191b214afc4a..99d93e526350 100644 --- a/addons/a11y/src/components/Panel.js +++ b/addons/a11y/src/components/Panel.js @@ -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 }) { diff --git a/addons/a11y/src/components/Report/index.js b/addons/a11y/src/components/Report/index.js index 3a118280a530..74dd1f1dbb92 100644 --- a/addons/a11y/src/components/Report/index.js +++ b/addons/a11y/src/components/Report/index.js @@ -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 }) => ( diff --git a/addons/a11y/src/components/WrapStory.js b/addons/a11y/src/components/WrapStory.js index 9260fe2efa5a..b36e8c15c862 100644 --- a/addons/a11y/src/components/WrapStory.js +++ b/addons/a11y/src/components/WrapStory.js @@ -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); } } diff --git a/addons/a11y/src/html.js b/addons/a11y/src/html.js new file mode 100644 index 000000000000..f41765366cce --- /dev/null +++ b/addons/a11y/src/html.js @@ -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(); +}; diff --git a/addons/a11y/src/shared/index.js b/addons/a11y/src/shared/index.js index bc1dd0326492..2bcaf9d19683 100755 --- a/addons/a11y/src/shared/index.js +++ b/addons/a11y/src/shared/index.js @@ -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 }; diff --git a/addons/actions/README.md b/addons/actions/README.md index cd025f3f3b5b..7b63d53cd156 100644 --- a/addons/actions/README.md +++ b/addons/actions/README.md @@ -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', () => ) @@ -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 `' '` 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', () => ` +
+ Clicks on this button will be logged: +
+ `); +``` diff --git a/addons/actions/package.json b/addons/actions/package.json index a3401fec1395..bf42bf220b17 100644 --- a/addons/actions/package.json +++ b/addons/actions/package.json @@ -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", diff --git a/addons/actions/src/index.js b/addons/actions/src/index.js index b3653735104a..35d2ab3d4a92 100644 --- a/addons/actions/src/index.js +++ b/addons/actions/src/index.js @@ -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 }; diff --git a/addons/actions/src/preview/decorateAction.js b/addons/actions/src/preview/decorateAction.js index 056a3202f20f..d61f3744874d 100644 --- a/addons/actions/src/preview/decorateAction.js +++ b/addons/actions/src/preview/decorateAction.js @@ -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), }; } diff --git a/addons/actions/src/preview/index.js b/addons/actions/src/preview/index.js index ec93018f9e64..3ac38c32c124 100644 --- a/addons/actions/src/preview/index.js +++ b/addons/actions/src/preview/index.js @@ -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'; diff --git a/addons/actions/src/preview/withActions.js b/addons/actions/src/preview/withActions.js new file mode 100644 index 000000000000..35b78f48d62c --- /dev/null +++ b/addons/actions/src/preview/withActions.js @@ -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); diff --git a/addons/background/README.md b/addons/backgrounds/README.md similarity index 100% rename from addons/background/README.md rename to addons/backgrounds/README.md diff --git a/addons/backgrounds/html.js b/addons/backgrounds/html.js new file mode 100644 index 000000000000..4f7edc6bedba --- /dev/null +++ b/addons/backgrounds/html.js @@ -0,0 +1 @@ +module.exports = require('./dist/html'); diff --git a/addons/background/mithril.js b/addons/backgrounds/mithril.js similarity index 100% rename from addons/background/mithril.js rename to addons/backgrounds/mithril.js diff --git a/addons/background/package.json b/addons/backgrounds/package.json similarity index 95% rename from addons/background/package.json rename to addons/backgrounds/package.json index 4bf9ea662b7c..f429c870b992 100644 --- a/addons/background/package.json +++ b/addons/backgrounds/package.json @@ -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", diff --git a/addons/background/register.js b/addons/backgrounds/register.js similarity index 100% rename from addons/background/register.js rename to addons/backgrounds/register.js diff --git a/addons/background/src/BackgroundPanel.js b/addons/backgrounds/src/BackgroundPanel.js similarity index 97% rename from addons/background/src/BackgroundPanel.js rename to addons/backgrounds/src/BackgroundPanel.js index 4e557636da25..14dbd9fa1539 100644 --- a/addons/background/src/BackgroundPanel.js +++ b/addons/backgrounds/src/BackgroundPanel.js @@ -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'); }); diff --git a/addons/background/src/Swatch.js b/addons/backgrounds/src/Swatch.js similarity index 100% rename from addons/background/src/Swatch.js rename to addons/backgrounds/src/Swatch.js diff --git a/addons/background/src/__tests__/BackgroundPanel.js b/addons/backgrounds/src/__tests__/BackgroundPanel.js similarity index 92% rename from addons/background/src/__tests__/BackgroundPanel.js rename to addons/backgrounds/src/__tests__/BackgroundPanel.js index b4282e55fb5a..59eb89db51ff 100644 --- a/addons/background/src/__tests__/BackgroundPanel.js +++ b/addons/backgrounds/src/__tests__/BackgroundPanel.js @@ -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(); - 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(); - 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(); - 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(); 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(); - 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); }); diff --git a/addons/background/src/__tests__/Swatch.js b/addons/backgrounds/src/__tests__/Swatch.js similarity index 100% rename from addons/background/src/__tests__/Swatch.js rename to addons/backgrounds/src/__tests__/Swatch.js diff --git a/addons/background/src/__tests__/index.js b/addons/backgrounds/src/__tests__/index.js similarity index 92% rename from addons/background/src/__tests__/index.js rename to addons/backgrounds/src/__tests__/index.js index 1a3fdb5c52f4..8da89469a59c 100644 --- a/addons/background/src/__tests__/index.js +++ b/addons/backgrounds/src/__tests__/index.js @@ -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 = () => () =>

Hello World!

; @@ -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(); diff --git a/addons/background/src/__tests__/vue.js b/addons/backgrounds/src/__tests__/vue.js similarity index 91% rename from addons/background/src/__tests__/vue.js rename to addons/backgrounds/src/__tests__/vue.js index 8cad3d7a4914..3893473dad61 100644 --- a/addons/background/src/__tests__/vue.js +++ b/addons/backgrounds/src/__tests__/vue.js @@ -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)); }); }); diff --git a/addons/backgrounds/src/events.js b/addons/backgrounds/src/events.js new file mode 100644 index 000000000000..25c9f6871f86 --- /dev/null +++ b/addons/backgrounds/src/events.js @@ -0,0 +1,6 @@ +const ADDON_ID = 'backgrounds'; + +export default { + SET: `${ADDON_ID}:set`, + UNSET: `${ADDON_ID}:unset`, +}; diff --git a/addons/backgrounds/src/html.js b/addons/backgrounds/src/html.js new file mode 100644 index 000000000000..20eeb3d1c6a7 --- /dev/null +++ b/addons/backgrounds/src/html.js @@ -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(); +}; diff --git a/addons/background/src/index.js b/addons/backgrounds/src/index.js similarity index 91% rename from addons/background/src/index.js rename to addons/backgrounds/src/index.js index 3fdf626475b4..64142e74b8b0 100644 --- a/addons/background/src/index.js +++ b/addons/backgrounds/src/index.js @@ -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() { diff --git a/addons/background/src/mithril.js b/addons/backgrounds/src/mithril.js similarity index 84% rename from addons/background/src/mithril.js rename to addons/backgrounds/src/mithril.js index a9fd92f71e79..8c27f1390dac 100644 --- a/addons/background/src/mithril.js +++ b/addons/backgrounds/src/mithril.js @@ -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() { diff --git a/addons/background/src/register.js b/addons/backgrounds/src/register.js similarity index 100% rename from addons/background/src/register.js rename to addons/backgrounds/src/register.js diff --git a/addons/background/src/vue.js b/addons/backgrounds/src/vue.js similarity index 80% rename from addons/background/src/vue.js rename to addons/backgrounds/src/vue.js index 8fb578c0d05e..5ff182695a70 100644 --- a/addons/background/src/vue.js +++ b/addons/backgrounds/src/vue.js @@ -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); }, }); diff --git a/addons/background/vue.js b/addons/backgrounds/vue.js similarity index 100% rename from addons/background/vue.js rename to addons/backgrounds/vue.js diff --git a/addons/centered/html.js b/addons/centered/html.js new file mode 100644 index 000000000000..4f7edc6bedba --- /dev/null +++ b/addons/centered/html.js @@ -0,0 +1 @@ +module.exports = require('./dist/html'); diff --git a/addons/centered/src/html.js b/addons/centered/src/html.js new file mode 100644 index 000000000000..b274b747e6f6 --- /dev/null +++ b/addons/centered/src/html.js @@ -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; +} diff --git a/addons/centered/src/mithril.js b/addons/centered/src/mithril.js index 682d947e8307..3edb0d0c1915 100644 --- a/addons/centered/src/mithril.js +++ b/addons/centered/src/mithril.js @@ -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: () => ( -
-
{m(storyFn())}
+
+
{m(storyFn())}
), }; diff --git a/addons/centered/src/react.js b/addons/centered/src/react.js index f95d2a10fccb..58c380d940dc 100644 --- a/addons/centered/src/react.js +++ b/addons/centered/src/react.js @@ -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 ( -
-
{storyFn()}
+
+
{storyFn()}
); } diff --git a/addons/centered/src/styles.js b/addons/centered/src/styles.js new file mode 100644 index 000000000000..911b33e980a6 --- /dev/null +++ b/addons/centered/src/styles.js @@ -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; diff --git a/addons/centered/src/vue.js b/addons/centered/src/vue.js index 713e7dfeb1de..177a4cdaf5a4 100644 --- a/addons/centered/src/vue.js +++ b/addons/centered/src/vue.js @@ -1,3 +1,5 @@ +import styles from './styles'; + export default function() { return { template: ` @@ -8,22 +10,7 @@ export default function() {
`, 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; }, }; } diff --git a/addons/events/html.js b/addons/events/html.js new file mode 100644 index 000000000000..4f7edc6bedba --- /dev/null +++ b/addons/events/html.js @@ -0,0 +1 @@ +module.exports = require('./dist/html'); diff --git a/addons/events/package.json b/addons/events/package.json index fc7431d3b1e2..f637c44f75da 100644 --- a/addons/events/package.json +++ b/addons/events/package.json @@ -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", diff --git a/addons/events/src/html.js b/addons/events/src/html.js new file mode 100644 index 000000000000..a7090b809c99 --- /dev/null +++ b/addons/events/src/html.js @@ -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(); +}; diff --git a/addons/knobs/html.js b/addons/knobs/html.js new file mode 100644 index 000000000000..4f7edc6bedba --- /dev/null +++ b/addons/knobs/html.js @@ -0,0 +1 @@ +module.exports = require('./dist/html'); diff --git a/addons/knobs/package.json b/addons/knobs/package.json index 8d755e6e7a96..00d46a898a4a 100644 --- a/addons/knobs/package.json +++ b/addons/knobs/package.json @@ -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", diff --git a/addons/knobs/src/html/index.js b/addons/knobs/src/html/index.js new file mode 100644 index 000000000000..a032f2979ea0 --- /dev/null +++ b/addons/knobs/src/html/index.js @@ -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 }); diff --git a/addons/knobs/src/html/registerKnobs.js b/addons/knobs/src/html/registerKnobs.js new file mode 100644 index 000000000000..b654e0fe4bf7 --- /dev/null +++ b/addons/knobs/src/html/registerKnobs.js @@ -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; diff --git a/addons/links/README.md b/addons/links/README.md index 4fd95ec68c50..e3e7c057b0be 100644 --- a/addons/links/README.md +++ b/addons/links/README.md @@ -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', () => ( + + )) +``` + ## 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. diff --git a/addons/links/package.json b/addons/links/package.json index 4f586d31ef96..e3d81abb6c87 100644 --- a/addons/links/package.json +++ b/addons/links/package.json @@ -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" diff --git a/addons/links/src/index.js b/addons/links/src/index.js index 1f8a4f52daf6..d5c82d4b3d4f 100644 --- a/addons/links/src/index.js +++ b/addons/links/src/index.js @@ -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; diff --git a/addons/links/src/preview.js b/addons/links/src/preview.js index bdb0558293b2..547a1759316d 100644 --- a/addons/links/src/preview.js +++ b/addons/links/src/preview.js @@ -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(); +}; diff --git a/addons/storyshots/src/frameworkLoader.js b/addons/storyshots/src/frameworkLoader.js index 047759ec3a98..0a8cdd51df07 100644 --- a/addons/storyshots/src/frameworkLoader.js +++ b/addons/storyshots/src/frameworkLoader.js @@ -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)); diff --git a/addons/storyshots/src/html/loader.js b/addons/storyshots/src/html/loader.js new file mode 100644 index 000000000000..634cc8395762 --- /dev/null +++ b/addons/storyshots/src/html/loader.js @@ -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, +}; diff --git a/addons/storyshots/src/html/renderTree.js b/addons/storyshots/src/html/renderTree.js new file mode 100644 index 000000000000..6f02f83ef253 --- /dev/null +++ b/addons/storyshots/src/html/renderTree.js @@ -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; diff --git a/app/html/.npmignore b/app/html/.npmignore new file mode 100644 index 000000000000..329fc8d67ad9 --- /dev/null +++ b/app/html/.npmignore @@ -0,0 +1,3 @@ +docs +src +.babelrc diff --git a/app/html/README.md b/app/html/README.md new file mode 100644 index 000000000000..9e09fedf8c95 --- /dev/null +++ b/app/html/README.md @@ -0,0 +1,26 @@ +# Storybook for HTML alpha + +* * * + +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. diff --git a/app/html/bin/build.js b/app/html/bin/build.js new file mode 100755 index 000000000000..780773c6cd31 --- /dev/null +++ b/app/html/bin/build.js @@ -0,0 +1,3 @@ +#!/usr/bin/env node + +require('../dist/server/build'); diff --git a/app/html/bin/index.js b/app/html/bin/index.js new file mode 100755 index 000000000000..2e96258ce63d --- /dev/null +++ b/app/html/bin/index.js @@ -0,0 +1,3 @@ +#!/usr/bin/env node + +require('../dist/server'); diff --git a/app/html/docs/demo.png b/app/html/docs/demo.png new file mode 100644 index 000000000000..4bb13aa62974 Binary files /dev/null and b/app/html/docs/demo.png differ diff --git a/app/html/package.json b/app/html/package.json new file mode 100644 index 000000000000..5ba09f7c5f27 --- /dev/null +++ b/app/html/package.json @@ -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" + } +} diff --git a/app/html/src/client/index.js b/app/html/src/client/index.js new file mode 100644 index 000000000000..88fb416f70ef --- /dev/null +++ b/app/html/src/client/index.js @@ -0,0 +1,9 @@ +export { + storiesOf, + setAddon, + addDecorator, + addParameters, + configure, + getStorybook, + forceReRender, +} from './preview'; diff --git a/app/html/src/client/preview/index.js b/app/html/src/client/preview/index.js new file mode 100644 index 000000000000..b04f8439b607 --- /dev/null +++ b/app/html/src/client/preview/index.js @@ -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 }; diff --git a/app/html/src/client/preview/render.js b/app/html/src/client/preview/render.js new file mode 100644 index 000000000000..75484ac511d5 --- /dev/null +++ b/app/html/src/client/preview/render.js @@ -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 "() => " or when defining the story. + `, + }); + } +} diff --git a/app/html/src/server/build.js b/app/html/src/server/build.js new file mode 100755 index 000000000000..804f258a0ba4 --- /dev/null +++ b/app/html/src/server/build.js @@ -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'), +}); diff --git a/app/html/src/server/config.js b/app/html/src/server/config.js new file mode 100644 index 000000000000..a031a5a47121 --- /dev/null +++ b/app/html/src/server/config.js @@ -0,0 +1,8 @@ +import { configLoaderCreator } from '@storybook/core/server'; +import defaultConfig from './config/babel'; + +const configLoader = configLoaderCreator({ + defaultBabelConfig: defaultConfig, +}); + +export default configLoader; diff --git a/app/html/src/server/config/babel.js b/app/html/src/server/config/babel.js new file mode 100644 index 000000000000..77a413a7cb48 --- /dev/null +++ b/app/html/src/server/config/babel.js @@ -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, + }, + ], + ], +}; diff --git a/app/html/src/server/config/babel.prod.js b/app/html/src/server/config/babel.prod.js new file mode 100644 index 000000000000..5ceaf68bae9f --- /dev/null +++ b/app/html/src/server/config/babel.prod.js @@ -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, + }, + ], + ], +}; diff --git a/app/html/src/server/config/globals.js b/app/html/src/server/config/globals.js new file mode 100644 index 000000000000..d889a4529761 --- /dev/null +++ b/app/html/src/server/config/globals.js @@ -0,0 +1,4 @@ +import { window } from 'global'; + +window.STORYBOOK_REACT_CLASSES = {}; +window.STORYBOOK_ENV = 'HTML'; diff --git a/app/html/src/server/config/polyfills.js b/app/html/src/server/config/polyfills.js new file mode 100644 index 000000000000..869b6824b5ff --- /dev/null +++ b/app/html/src/server/config/polyfills.js @@ -0,0 +1,3 @@ +import 'core-js/es6/symbol'; +import 'core-js/fn/array/iterator'; +import 'airbnb-js-shims'; diff --git a/app/html/src/server/config/utils.js b/app/html/src/server/config/utils.js new file mode 100644 index 000000000000..fc73c38a37f0 --- /dev/null +++ b/app/html/src/server/config/utils.js @@ -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, + }; +} diff --git a/app/html/src/server/config/webpack.config.js b/app/html/src/server/config/webpack.config.js new file mode 100644 index 000000000000..c572bcba82d1 --- /dev/null +++ b/app/html/src/server/config/webpack.config.js @@ -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; +} diff --git a/app/html/src/server/config/webpack.config.prod.js b/app/html/src/server/config/webpack.config.prod.js new file mode 100644 index 000000000000..8ae4ce9421d8 --- /dev/null +++ b/app/html/src/server/config/webpack.config.prod.js @@ -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; +} diff --git a/app/html/src/server/index.js b/app/html/src/server/index.js new file mode 100755 index 000000000000..69df1fe59e54 --- /dev/null +++ b/app/html/src/server/index.js @@ -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'), +}); diff --git a/app/html/src/server/public/favicon.ico b/app/html/src/server/public/favicon.ico new file mode 100755 index 000000000000..e1cf7f1c59fd Binary files /dev/null and b/app/html/src/server/public/favicon.ico differ diff --git a/app/polymer/src/client/preview/render.js b/app/polymer/src/client/preview/render.js index 201966de513b..6ed2e5a9bce3 100644 --- a/app/polymer/src/client/preview/render.js +++ b/app/polymer/src/client/preview/render.js @@ -9,8 +9,8 @@ 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 "() => '<your-component-name></your-component-name\>'" when defining the story. `, diff --git a/app/vue/src/client/preview/render.js b/app/vue/src/client/preview/render.js index 7c865949b656..f6a4dbf09183 100644 --- a/app/vue/src/client/preview/render.js +++ b/app/vue/src/client/preview/render.js @@ -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: '' })" or "() => ({ components: MyComp, template: '' })" when defining the story. `, diff --git a/docs/src/pages/addons/addon-gallery/index.md b/docs/src/pages/addons/addon-gallery/index.md index e6d5db59ae19..0ee37d0b6a84 100644 --- a/docs/src/pages/addons/addon-gallery/index.md +++ b/docs/src/pages/addons/addon-gallery/index.md @@ -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. diff --git a/docs/src/pages/basics/guide-html/index.md b/docs/src/pages/basics/guide-html/index.md new file mode 100644 index 000000000000..398dc67624ca --- /dev/null +++ b/docs/src/pages/basics/guide-html/index.md @@ -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. +That’s 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', () => '

Hello World

') + .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. diff --git a/docs/src/pages/basics/live-examples/index.md b/docs/src/pages/basics/live-examples/index.md index 425630295b10..f23d9e276e3a 100644 --- a/docs/src/pages/basics/live-examples/index.md +++ b/docs/src/pages/basics/live-examples/index.md @@ -11,6 +11,7 @@ title: 'Live Examples' - [Angular](https://storybooks-angular.netlify.com/) - [Polymer](https://storybooks-polymer.netlify.com/) - [Mithril](https://storybooks-mithril.netlify.com/) +- [HTML](https://storybooks-html.netlify.com/) ### 3.4 - [React Official](https://release-3-4--storybooks-official.netlify.com) diff --git a/docs/src/pages/basics/quick-start-guide/index.md b/docs/src/pages/basics/quick-start-guide/index.md index c1d889380cfb..68f4b11f10ac 100644 --- a/docs/src/pages/basics/quick-start-guide/index.md +++ b/docs/src/pages/basics/quick-start-guide/index.md @@ -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). diff --git a/docs/src/pages/basics/slow-start-guide/index.md b/docs/src/pages/basics/slow-start-guide/index.md index 46370c1f2782..80b9c32946c1 100644 --- a/docs/src/pages/basics/slow-start-guide/index.md +++ b/docs/src/pages/basics/slow-start-guide/index.md @@ -9,3 +9,4 @@ Storybook supports multiple UI libraries. The manual setup for each is different - [Storybook for Vue](/basics/guide-vue/) - [Storybook for Angular](/basics/guide-angular/) - [Storybook for Mithril](/basics/guide-mithril/) +- [Storybook for HTML](/basics/guide-html/) diff --git a/examples/angular-cli/src/app/app.component.spec.ts b/examples/angular-cli/src/app/app.component.spec.ts index 32ab9b0638a1..f22ed943cf7f 100644 --- a/examples/angular-cli/src/app/app.component.spec.ts +++ b/examples/angular-cli/src/app/app.component.spec.ts @@ -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!'); + })); }); diff --git a/examples/html-kitchen-sink/.storybook/addons.js b/examples/html-kitchen-sink/.storybook/addons.js new file mode 100644 index 000000000000..c38a31c6ea86 --- /dev/null +++ b/examples/html-kitchen-sink/.storybook/addons.js @@ -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'; diff --git a/examples/html-kitchen-sink/.storybook/config.js b/examples/html-kitchen-sink/.storybook/config.js new file mode 100644 index 000000000000..d67bc8b54626 --- /dev/null +++ b/examples/html-kitchen-sink/.storybook/config.js @@ -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); diff --git a/examples/html-kitchen-sink/.storybook/webpack.config.js b/examples/html-kitchen-sink/.storybook/webpack.config.js new file mode 100644 index 000000000000..5d9aaf4e1dad --- /dev/null +++ b/examples/html-kitchen-sink/.storybook/webpack.config.js @@ -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; +}; diff --git a/examples/html-kitchen-sink/package.json b/examples/html-kitchen-sink/package.json new file mode 100644 index 000000000000..78b303d5c0f6 --- /dev/null +++ b/examples/html-kitchen-sink/package.json @@ -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" + } +} diff --git a/examples/html-kitchen-sink/stories/__snapshots__/addon-a11y.stories.storyshot b/examples/html-kitchen-sink/stories/__snapshots__/addon-a11y.stories.storyshot new file mode 100644 index 000000000000..8c979f07574a --- /dev/null +++ b/examples/html-kitchen-sink/stories/__snapshots__/addon-a11y.stories.storyshot @@ -0,0 +1,27 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots Addons|a11y Default 1`] = ` +`; + +exports[`Storyshots Addons|a11y Invalid contrast 1`] = ` + +`; + +exports[`Storyshots Addons|a11y Label 1`] = ` + +`; diff --git a/examples/html-kitchen-sink/stories/__snapshots__/addon-actions.stories.storyshot b/examples/html-kitchen-sink/stories/__snapshots__/addon-actions.stories.storyshot new file mode 100644 index 000000000000..b5a33580d65d --- /dev/null +++ b/examples/html-kitchen-sink/stories/__snapshots__/addon-actions.stories.storyshot @@ -0,0 +1,62 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots Addons|Actions Decorated actions + config 1`] = ` + +`; + +exports[`Storyshots Addons|Actions Decorated actions 1`] = ` + +`; + +exports[`Storyshots Addons|Actions Hello World 1`] = ` + +`; + +exports[`Storyshots Addons|Actions Multiple actions + config 1`] = ` + +`; + +exports[`Storyshots Addons|Actions Multiple actions 1`] = ` + +`; + +exports[`Storyshots Addons|Actions Multiple actions, object + config 1`] = ` + +`; + +exports[`Storyshots Addons|Actions Multiple actions, object 1`] = ` + +`; + +exports[`Storyshots Addons|Actions Multiple actions, selector 1`] = ` + + +`; diff --git a/examples/html-kitchen-sink/stories/__snapshots__/addon-backgrounds.stories.storyshot b/examples/html-kitchen-sink/stories/__snapshots__/addon-backgrounds.stories.storyshot new file mode 100644 index 000000000000..211e0b453450 --- /dev/null +++ b/examples/html-kitchen-sink/stories/__snapshots__/addon-backgrounds.stories.storyshot @@ -0,0 +1,17 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots Addons|Backgrounds story 1 1`] = ` + + You should be able to switch backgrounds for this story + +`; + +exports[`Storyshots Addons|Backgrounds story 2 1`] = ` + + This one too! + +`; diff --git a/examples/html-kitchen-sink/stories/__snapshots__/addon-centered.stories.storyshot b/examples/html-kitchen-sink/stories/__snapshots__/addon-centered.stories.storyshot new file mode 100644 index 000000000000..177b0ea3a16e --- /dev/null +++ b/examples/html-kitchen-sink/stories/__snapshots__/addon-centered.stories.storyshot @@ -0,0 +1,17 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots Addons|Centered button in center 1`] = ` +
+
+ +
+
+`; diff --git a/examples/html-kitchen-sink/stories/__snapshots__/addon-events.stories.storyshot b/examples/html-kitchen-sink/stories/__snapshots__/addon-events.stories.storyshot new file mode 100644 index 000000000000..84466dd440ae --- /dev/null +++ b/examples/html-kitchen-sink/stories/__snapshots__/addon-events.stories.storyshot @@ -0,0 +1,6 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots Addons|Events Logger 1`] = ` + + +`; diff --git a/examples/html-kitchen-sink/stories/__snapshots__/addon-jest.stories.storyshot b/examples/html-kitchen-sink/stories/__snapshots__/addon-jest.stories.storyshot new file mode 100644 index 000000000000..30260c566cae --- /dev/null +++ b/examples/html-kitchen-sink/stories/__snapshots__/addon-jest.stories.storyshot @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots Addons|jest withTests 1`] = `This story shows test results`; diff --git a/examples/html-kitchen-sink/stories/__snapshots__/addon-knobs.stories.storyshot b/examples/html-kitchen-sink/stories/__snapshots__/addon-knobs.stories.storyshot new file mode 100644 index 000000000000..73f9636e4b14 --- /dev/null +++ b/examples/html-kitchen-sink/stories/__snapshots__/addon-knobs.stories.storyshot @@ -0,0 +1,54 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots Addons|Knobs All knobs 1`] = ` +
+ + +

+ My name is Jane, +

+ + +

+ today is January 20, 2017 +

+ + +

+ I have a stock of 20 apple, costing $2.25 each. +

+ + +

+ Also, I have: +

+ + +
    +
  • + Laptop +
  • +
  • + Book +
  • +
  • + Whiskey +
  • +
+ + +

+ Nice to meet you! +

+ + +
+`; + +exports[`Storyshots Addons|Knobs Simple 1`] = ` +
+ I am John Doe and I'm 44 years old. +
+`; diff --git a/examples/html-kitchen-sink/stories/__snapshots__/addon-notes.stories.storyshot b/examples/html-kitchen-sink/stories/__snapshots__/addon-notes.stories.storyshot new file mode 100644 index 000000000000..b7b58fa7badb --- /dev/null +++ b/examples/html-kitchen-sink/stories/__snapshots__/addon-notes.stories.storyshot @@ -0,0 +1,15 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots Addons|Notes Simple note 1`] = ` +

+ + + + + This is a fragment of HTML + + + + +

+`; diff --git a/examples/html-kitchen-sink/stories/__snapshots__/index.stories.storyshot b/examples/html-kitchen-sink/stories/__snapshots__/index.stories.storyshot new file mode 100644 index 000000000000..9fc155940224 --- /dev/null +++ b/examples/html-kitchen-sink/stories/__snapshots__/index.stories.storyshot @@ -0,0 +1,130 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots Demo button 1`] = ` + +`; + +exports[`Storyshots Demo heading 1`] = ` +

+ Hello World +

+`; + +exports[`Storyshots Demo headings 1`] = ` +
+

+ Hello World +

+

+ Hello World +

+

+ Hello World +

+

+ Hello World +

+
+`; + +exports[`Storyshots Welcome Welcome 1`] = ` +
+ + +

+ Welcome to Storybook for HTML +

+ + +

+ This is a UI component dev environment for your plain HTML snippets. +

+ + +

+ + We've added some basic stories inside the + + stories + + directory. + +
+ + A story is a single state of one or more UI components. You can have as many stories as you want. + +
+ + (Basically a story is like a visual test case.) + +

+ + +

+ + See these sample + + stories + + + +

+ + +

+ +

+ + +

+ + Just like that, you can add your own snippets as stories. + +
+ + You can also edit those snippets and see changes right away. + +
+ + +

+ + +

+ + Usually we create stories with smaller UI components in the app. +
+ + Have a look at the + + + + Writing Stories + + + + section in our documentation. + +

+ + +
+`; diff --git a/examples/html-kitchen-sink/stories/addon-a11y.stories.js b/examples/html-kitchen-sink/stories/addon-a11y.stories.js new file mode 100644 index 000000000000..7341d03e8a6a --- /dev/null +++ b/examples/html-kitchen-sink/stories/addon-a11y.stories.js @@ -0,0 +1,28 @@ +import { document, setTimeout } from 'global'; +import { storiesOf } from '@storybook/html'; +import { setOptions } from '@storybook/addon-options'; + +import { checkA11y } from '@storybook/addon-a11y/html'; + +const text = 'Testing the a11y addon'; + +storiesOf('Addons|a11y', module) + .addDecorator(checkA11y) + .addDecorator(fn => { + setOptions({ selectedAddonPanel: '@storybook/addon-a11y/panel' }); + return fn(); + }) + .add('Default', () => ``) + .add('Label', () => ``) + .add('Disabled', () => ``) + .add( + 'Invalid contrast', + () => `` + ) + .add('Delayed render', () => { + const div = document.createElement('div'); + setTimeout(() => { + div.innerHTML = ``; + }, 1000); + return div; + }); diff --git a/examples/html-kitchen-sink/stories/addon-actions.stories.js b/examples/html-kitchen-sink/stories/addon-actions.stories.js new file mode 100644 index 000000000000..09f606d58dd4 --- /dev/null +++ b/examples/html-kitchen-sink/stories/addon-actions.stories.js @@ -0,0 +1,34 @@ +import { storiesOf } from '@storybook/html'; +import { withActions, decorate } from '@storybook/addon-actions'; + +const pickTarget = decorate([args => [args[0].target]]); + +const button = () => ``; + +storiesOf('Addons|Actions', module) + .add('Hello World', () => withActions('click')(button)) + .add('Multiple actions', () => withActions('click', 'contextmenu')(button)) + .add('Multiple actions + config', () => + withActions('click', 'contextmenu', { clearOnStoryChange: false })(button) + ) + .add('Multiple actions, object', () => + withActions({ click: 'clicked', contextmenu: 'right clicked' })(button) + ) + .add('Multiple actions, selector', () => + withActions({ 'click .btn': 'clicked', contextmenu: 'right clicked' })( + () => ` +
+ Clicks on this button will be logged: +
+ ` + ) + ) + .add('Multiple actions, object + config', () => + withActions({ click: 'clicked', contextmenu: 'right clicked' }, { clearOnStoryChange: false })( + button + ) + ) + .add('Decorated actions', () => pickTarget.withActions('click', 'contextmenu')(button)) + .add('Decorated actions + config', () => + pickTarget.withActions('click', 'contextmenu', { clearOnStoryChange: false })(button) + ); diff --git a/examples/html-kitchen-sink/stories/addon-backgrounds.stories.js b/examples/html-kitchen-sink/stories/addon-backgrounds.stories.js new file mode 100644 index 000000000000..1c8eed6ac6dc --- /dev/null +++ b/examples/html-kitchen-sink/stories/addon-backgrounds.stories.js @@ -0,0 +1,17 @@ +import { storiesOf } from '@storybook/html'; + +import backgrounds from '@storybook/addon-backgrounds/html'; + +storiesOf('Addons|Backgrounds', module) + .addDecorator( + backgrounds([ + { name: 'twitter', value: '#00aced' }, + { name: 'facebook', value: '#3b5998', default: true }, + ]) + ) + .add( + 'story 1', + () => + 'You should be able to switch backgrounds for this story' + ) + .add('story 2', () => 'This one too!'); diff --git a/examples/html-kitchen-sink/stories/addon-centered.stories.js b/examples/html-kitchen-sink/stories/addon-centered.stories.js new file mode 100644 index 000000000000..49edcdfb4821 --- /dev/null +++ b/examples/html-kitchen-sink/stories/addon-centered.stories.js @@ -0,0 +1,6 @@ +import { storiesOf } from '@storybook/html'; +import centered from '@storybook/addon-centered/html'; + +storiesOf('Addons|Centered', module) + .addDecorator(centered) + .add('button in center', () => ''); diff --git a/examples/html-kitchen-sink/stories/addon-events.css b/examples/html-kitchen-sink/stories/addon-events.css new file mode 100644 index 000000000000..1346e8b132c3 --- /dev/null +++ b/examples/html-kitchen-sink/stories/addon-events.css @@ -0,0 +1,16 @@ +.wrapper { + padding: 20px; + font-family: + -apple-system, ".SFNSText-Regular", "San Francisco", "Roboto", + "Segoe UI", "Helvetica Neue", "Lucida Grande", sans-serif; + color: rgb(51, 51, 51); +} + +.title { + margin: 0; +} + +.item { + list-style: none; + margin-bottom: 10px; +} diff --git a/examples/html-kitchen-sink/stories/addon-events.stories.js b/examples/html-kitchen-sink/stories/addon-events.stories.js new file mode 100644 index 000000000000..f0be1bee0a0e --- /dev/null +++ b/examples/html-kitchen-sink/stories/addon-events.stories.js @@ -0,0 +1,115 @@ +import EventEmitter from 'eventemitter3'; +import { storiesOf } from '@storybook/html'; +import addons from '@storybook/addons'; +import CoreEvents from '@storybook/core-events'; +import json from 'format-json'; + +import withEvents from '@storybook/addon-events/html'; + +import './addon-events.css'; + +const TEST_EVENTS = { + TEST_EVENT_1: 'test-event-1', + TEST_EVENT_2: 'test-event-2', + TEST_EVENT_3: 'test-event-3', + TEST_EVENT_4: 'test-event-4', +}; + +const emitter = new EventEmitter(); +const emit = emitter.emit.bind(emitter); + +const events = []; +const eventHandlers = Object.values(TEST_EVENTS).map(name => ({ + name, + handler: payload => { + events.push({ name, payload }); + addons.getChannel().emit(CoreEvents.FORCE_RE_RENDER); + }, +})); + +const subscription = () => { + eventHandlers.forEach(({ name, handler }) => emitter.on(name, handler)); + return () => eventHandlers.forEach(({ name, handler }) => emitter.removeListener(name, handler)); +}; + +storiesOf('Addons|Events', module) + .addDecorator( + withEvents({ + emit, + events: [ + { + name: TEST_EVENTS.TEST_EVENT_1, + title: 'Test event 1', + payload: 0, + }, + { + name: TEST_EVENTS.TEST_EVENT_2, + title: 'Test event 2', + payload: 'Test event 2', + }, + { + name: TEST_EVENTS.TEST_EVENT_3, + title: 'Test event 3', + payload: { + string: 'value', + number: 123, + array: [1, 2, 3], + object: { + string: 'value', + number: 123, + array: [1, 2, 3], + }, + }, + }, + { + name: TEST_EVENTS.TEST_EVENT_4, + title: 'Test event 4', + payload: [ + { + string: 'value', + number: 123, + array: [1, 2, 3], + }, + { + string: 'value', + number: 123, + array: [1, 2, 3], + }, + { + string: 'value', + number: 123, + array: [1, 2, 3], + }, + ], + }, + ], + }) + ) + .addDecorator(story => { + addons.getChannel().emit(CoreEvents.REGISTER_SUBSCRIPTION, subscription); + return story(); + }) + .add( + 'Logger', + () => ` +
+

Logger

+
+ ${events + .map( + ({ name, payload }) => ` +
+
+ Event name: ${name} +
+
+ Event payload: ${json.plain(payload)} +
+
+ ` + ) + .join('')} +
+
+ ` + ); diff --git a/examples/html-kitchen-sink/stories/addon-jest.stories.js b/examples/html-kitchen-sink/stories/addon-jest.stories.js new file mode 100644 index 000000000000..543cc5642312 --- /dev/null +++ b/examples/html-kitchen-sink/stories/addon-jest.stories.js @@ -0,0 +1,12 @@ +import { storiesOf } from '@storybook/html'; + +import { withTests } from '@storybook/addon-jest'; +import results from './addon-jest.testresults.json'; + +const withTestsFiles = withTests({ + results, +}); + +storiesOf('Addons|jest', module) + .addDecorator(withTestsFiles('addon-jest')) + .add('withTests', () => 'This story shows test results'); diff --git a/examples/html-kitchen-sink/stories/addon-jest.testresults.json b/examples/html-kitchen-sink/stories/addon-jest.testresults.json new file mode 100644 index 000000000000..14cb9a48d08f --- /dev/null +++ b/examples/html-kitchen-sink/stories/addon-jest.testresults.json @@ -0,0 +1 @@ +{"numFailedTestSuites":1,"numFailedTests":4,"numPassedTestSuites":0,"numPassedTests":3,"numPendingTestSuites":0,"numPendingTests":0,"numRuntimeErrorTestSuites":0,"numTotalTestSuites":1,"numTotalTests":7,"snapshot":{"added":0,"didUpdate":false,"failure":false,"filesAdded":0,"filesRemoved":0,"filesUnmatched":0,"filesUpdated":0,"matched":0,"total":0,"unchecked":0,"uncheckedKeys":[],"unmatched":0,"updated":0},"startTime":1525471117521,"success":false,"testResults":[{"assertionResults":[{"ancestorTitles":[],"failureMessages":[],"fullName":"true should be true","location":null,"status":"passed","title":"true should be true"},{"ancestorTitles":["In a describe: "],"failureMessages":[],"fullName":"In a describe: true should still be true","location":null,"status":"passed","title":"true should still be true"},{"ancestorTitles":["In a describe: "],"failureMessages":[],"fullName":"In a describe: a list should contain 3 items","location":null,"status":"passed","title":"a list should contain 3 items"},{"ancestorTitles":["In a describe: "],"failureMessages":["Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).toEqual(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m\n\nExpected value to equal:\n \u001b[32m\"everything is awesome\"\u001b[39m\nReceived:\n \u001b[31m\"everything is all right\"\u001b[39m\n at Object. (/Users/jetbrains/IdeaProjects/storybook/examples/official-storybook/tests/addon-jest.test.js:16:39)\n at Object.asyncFn (/Users/jetbrains/IdeaProjects/storybook/node_modules/jest-jasmine2/build/jasmine_async.js:82:37)\n at resolve (/Users/jetbrains/IdeaProjects/storybook/node_modules/jest-jasmine2/build/queue_runner.js:52:12)\n at new Promise ()\n at mapper (/Users/jetbrains/IdeaProjects/storybook/node_modules/jest-jasmine2/build/queue_runner.js:39:19)\n at promise.then (/Users/jetbrains/IdeaProjects/storybook/node_modules/jest-jasmine2/build/queue_runner.js:73:82)\n at \n at process._tickCallback (internal/process/next_tick.js:188:7)"],"fullName":"In a describe: everything is awesome","location":null,"status":"failed","title":"everything is awesome"},{"ancestorTitles":["A bunch of failing tests: "],"failureMessages":["Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).toBe(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected value to be:\n \u001b[32mfalse\u001b[39m\nReceived:\n \u001b[31mtrue\u001b[39m\n at Object. (/Users/jetbrains/IdeaProjects/storybook/examples/official-storybook/tests/addon-jest.test.js:22:18)\n at Object.asyncFn (/Users/jetbrains/IdeaProjects/storybook/node_modules/jest-jasmine2/build/jasmine_async.js:82:37)\n at resolve (/Users/jetbrains/IdeaProjects/storybook/node_modules/jest-jasmine2/build/queue_runner.js:52:12)\n at new Promise ()\n at mapper (/Users/jetbrains/IdeaProjects/storybook/node_modules/jest-jasmine2/build/queue_runner.js:39:19)\n at promise.then (/Users/jetbrains/IdeaProjects/storybook/node_modules/jest-jasmine2/build/queue_runner.js:73:82)\n at \n at process._tickCallback (internal/process/next_tick.js:188:7)"],"fullName":"A bunch of failing tests: true should still be true","location":null,"status":"failed","title":"true should still be true"},{"ancestorTitles":["A bunch of failing tests: "],"failureMessages":["Error: \u001b[2mexpect(\u001b[22m\u001b[31marray\u001b[39m\u001b[2m).toContain(\u001b[22m\u001b[32mvalue\u001b[39m\u001b[2m)\u001b[22m\n\nExpected array:\n \u001b[31m[\"a\", \"b\", \"3\"]\u001b[39m\nTo contain value:\n \u001b[32m301\u001b[39m\n at Object. (/Users/jetbrains/IdeaProjects/storybook/examples/official-storybook/tests/addon-jest.test.js:26:29)\n at Object.asyncFn (/Users/jetbrains/IdeaProjects/storybook/node_modules/jest-jasmine2/build/jasmine_async.js:82:37)\n at resolve (/Users/jetbrains/IdeaProjects/storybook/node_modules/jest-jasmine2/build/queue_runner.js:52:12)\n at new Promise ()\n at mapper (/Users/jetbrains/IdeaProjects/storybook/node_modules/jest-jasmine2/build/queue_runner.js:39:19)\n at promise.then (/Users/jetbrains/IdeaProjects/storybook/node_modules/jest-jasmine2/build/queue_runner.js:73:82)\n at \n at process._tickCallback (internal/process/next_tick.js:188:7)"],"fullName":"A bunch of failing tests: a list should contain 3 items","location":null,"status":"failed","title":"a list should contain 3 items"},{"ancestorTitles":["A bunch of failing tests: "],"failureMessages":["Error: \u001b[2mexpect(\u001b[22m\u001b[31mfunction\u001b[39m\u001b[2m).toThrow(\u001b[22m\u001b[32mundefined\u001b[39m\u001b[2m)\u001b[22m\n\nExpected the function to throw an error.\nBut it didn't throw anything.\n at Object. (/Users/jetbrains/IdeaProjects/storybook/examples/official-storybook/tests/addon-jest.test.js:30:28)\n at Object.asyncFn (/Users/jetbrains/IdeaProjects/storybook/node_modules/jest-jasmine2/build/jasmine_async.js:82:37)\n at resolve (/Users/jetbrains/IdeaProjects/storybook/node_modules/jest-jasmine2/build/queue_runner.js:52:12)\n at new Promise ()\n at mapper (/Users/jetbrains/IdeaProjects/storybook/node_modules/jest-jasmine2/build/queue_runner.js:39:19)\n at promise.then (/Users/jetbrains/IdeaProjects/storybook/node_modules/jest-jasmine2/build/queue_runner.js:73:82)\n at \n at process._tickCallback (internal/process/next_tick.js:188:7)"],"fullName":"A bunch of failing tests: should work","location":null,"status":"failed","title":"should work"}],"endTime":1525471122009,"message":"\u001b[1m\u001b[31m \u001b[1m● \u001b[1mIn a describe: › everything is awesome\u001b[39m\u001b[22m\n\n \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).toEqual(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m\n \n Expected value to equal:\n \u001b[32m\"everything is awesome\"\u001b[39m\n Received:\n \u001b[31m\"everything is all right\"\u001b[39m\n\u001b[2m\u001b[22m\n\u001b[2m \u001b[0m \u001b[90m 14 | \u001b[39m\u001b[0m\u001b[22m\n\u001b[2m \u001b[0m \u001b[90m 15 | \u001b[39m test(\u001b[32m'everything is awesome'\u001b[39m\u001b[33m,\u001b[39m () \u001b[33m=>\u001b[39m {\u001b[0m\u001b[22m\n\u001b[2m \u001b[0m\u001b[31m\u001b[1m>\u001b[2m\u001b[39m\u001b[90m 16 | \u001b[39m expect(\u001b[32m'everything is all right'\u001b[39m)\u001b[33m.\u001b[39mtoEqual(\u001b[32m'everything is awesome'\u001b[39m)\u001b[33m;\u001b[39m\u001b[0m\u001b[22m\n\u001b[2m \u001b[0m \u001b[90m 17 | \u001b[39m })\u001b[33m;\u001b[39m\u001b[0m\u001b[22m\n\u001b[2m \u001b[0m \u001b[90m 18 | \u001b[39m})\u001b[33m;\u001b[39m\u001b[0m\u001b[22m\n\u001b[2m \u001b[0m \u001b[90m 19 | \u001b[39m\u001b[0m\u001b[22m\n\u001b[2m \u001b[22m\n\u001b[2m \u001b[2mat Object. (\u001b[2m\u001b[0m\u001b[36mexamples/official-storybook/tests/addon-jest.test.js\u001b[39m\u001b[0m\u001b[2m:16:39)\u001b[2m\u001b[22m\n\n\u001b[1m\u001b[31m \u001b[1m● \u001b[1mA bunch of failing tests: › true should still be true\u001b[39m\u001b[22m\n\n \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).toBe(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n \n Expected value to be:\n \u001b[32mfalse\u001b[39m\n Received:\n \u001b[31mtrue\u001b[39m\n\u001b[2m\u001b[22m\n\u001b[2m \u001b[0m \u001b[90m 20 | \u001b[39mdescribe(\u001b[32m'A bunch of failing tests: '\u001b[39m\u001b[33m,\u001b[39m () \u001b[33m=>\u001b[39m {\u001b[0m\u001b[22m\n\u001b[2m \u001b[0m \u001b[90m 21 | \u001b[39m test(\u001b[32m'true should still be true'\u001b[39m\u001b[33m,\u001b[39m () \u001b[33m=>\u001b[39m {\u001b[0m\u001b[22m\n\u001b[2m \u001b[0m\u001b[31m\u001b[1m>\u001b[2m\u001b[39m\u001b[90m 22 | \u001b[39m expect(\u001b[36mtrue\u001b[39m)\u001b[33m.\u001b[39mtoBe(\u001b[36mfalse\u001b[39m)\u001b[33m;\u001b[39m\u001b[0m\u001b[22m\n\u001b[2m \u001b[0m \u001b[90m 23 | \u001b[39m })\u001b[33m;\u001b[39m\u001b[0m\u001b[22m\n\u001b[2m \u001b[0m \u001b[90m 24 | \u001b[39m\u001b[0m\u001b[22m\n\u001b[2m \u001b[0m \u001b[90m 25 | \u001b[39m test(\u001b[32m'a list should contain 3 items'\u001b[39m\u001b[33m,\u001b[39m () \u001b[33m=>\u001b[39m {\u001b[0m\u001b[22m\n\u001b[2m \u001b[22m\n\u001b[2m \u001b[2mat Object. (\u001b[2m\u001b[0m\u001b[36mexamples/official-storybook/tests/addon-jest.test.js\u001b[39m\u001b[0m\u001b[2m:22:18)\u001b[2m\u001b[22m\n\n\u001b[1m\u001b[31m \u001b[1m● \u001b[1mA bunch of failing tests: › a list should contain 3 items\u001b[39m\u001b[22m\n\n \u001b[2mexpect(\u001b[22m\u001b[31marray\u001b[39m\u001b[2m).toContain(\u001b[22m\u001b[32mvalue\u001b[39m\u001b[2m)\u001b[22m\n \n Expected array:\n \u001b[31m[\"a\", \"b\", \"3\"]\u001b[39m\n To contain value:\n \u001b[32m301\u001b[39m\n\u001b[2m\u001b[22m\n\u001b[2m \u001b[0m \u001b[90m 24 | \u001b[39m\u001b[0m\u001b[22m\n\u001b[2m \u001b[0m \u001b[90m 25 | \u001b[39m test(\u001b[32m'a list should contain 3 items'\u001b[39m\u001b[33m,\u001b[39m () \u001b[33m=>\u001b[39m {\u001b[0m\u001b[22m\n\u001b[2m \u001b[0m\u001b[31m\u001b[1m>\u001b[2m\u001b[39m\u001b[90m 26 | \u001b[39m expect([\u001b[32m'a'\u001b[39m\u001b[33m,\u001b[39m \u001b[32m'b'\u001b[39m\u001b[33m,\u001b[39m \u001b[32m'3'\u001b[39m])\u001b[33m.\u001b[39mtoContain(\u001b[35m301\u001b[39m)\u001b[33m;\u001b[39m\u001b[0m\u001b[22m\n\u001b[2m \u001b[0m \u001b[90m 27 | \u001b[39m })\u001b[33m;\u001b[39m\u001b[0m\u001b[22m\n\u001b[2m \u001b[0m \u001b[90m 28 | \u001b[39m\u001b[0m\u001b[22m\n\u001b[2m \u001b[0m \u001b[90m 29 | \u001b[39m test(\u001b[32m'should work'\u001b[39m\u001b[33m,\u001b[39m () \u001b[33m=>\u001b[39m {\u001b[0m\u001b[22m\n\u001b[2m \u001b[22m\n\u001b[2m \u001b[2mat Object. (\u001b[2m\u001b[0m\u001b[36mexamples/official-storybook/tests/addon-jest.test.js\u001b[39m\u001b[0m\u001b[2m:26:29)\u001b[2m\u001b[22m\n\n\u001b[1m\u001b[31m \u001b[1m● \u001b[1mA bunch of failing tests: › should work\u001b[39m\u001b[22m\n\n \u001b[2mexpect(\u001b[22m\u001b[31mfunction\u001b[39m\u001b[2m).toThrow(\u001b[22m\u001b[32mundefined\u001b[39m\u001b[2m)\u001b[22m\n \n Expected the function to throw an error.\n But it didn't throw anything.\n\u001b[2m\u001b[22m\n\u001b[2m \u001b[0m \u001b[90m 28 | \u001b[39m\u001b[0m\u001b[22m\n\u001b[2m \u001b[0m \u001b[90m 29 | \u001b[39m test(\u001b[32m'should work'\u001b[39m\u001b[33m,\u001b[39m () \u001b[33m=>\u001b[39m {\u001b[0m\u001b[22m\n\u001b[2m \u001b[0m\u001b[31m\u001b[1m>\u001b[2m\u001b[39m\u001b[90m 30 | \u001b[39m expect(() \u001b[33m=>\u001b[39m {})\u001b[33m.\u001b[39mtoThrow()\u001b[33m;\u001b[39m\u001b[0m\u001b[22m\n\u001b[2m \u001b[0m \u001b[90m 31 | \u001b[39m })\u001b[33m;\u001b[39m\u001b[0m\u001b[22m\n\u001b[2m \u001b[0m \u001b[90m 32 | \u001b[39m})\u001b[33m;\u001b[39m\u001b[0m\u001b[22m\n\u001b[2m \u001b[0m \u001b[90m 33 | \u001b[39m\u001b[0m\u001b[22m\n\u001b[2m \u001b[22m\n\u001b[2m \u001b[2mat Object. (\u001b[2m\u001b[0m\u001b[36mexamples/official-storybook/tests/addon-jest.test.js\u001b[39m\u001b[0m\u001b[2m:30:28)\u001b[2m\u001b[22m\n","name":"/Users/jetbrains/IdeaProjects/storybook/examples/official-storybook/tests/addon-jest.test.js","startTime":1525471118647,"status":"failed","summary":""}],"wasInterrupted":false} \ No newline at end of file diff --git a/examples/html-kitchen-sink/stories/addon-knobs.stories.js b/examples/html-kitchen-sink/stories/addon-knobs.stories.js new file mode 100644 index 000000000000..406ba3acf184 --- /dev/null +++ b/examples/html-kitchen-sink/stories/addon-knobs.stories.js @@ -0,0 +1,66 @@ +import { storiesOf } from '@storybook/html'; +import { action } from '@storybook/addon-actions'; + +import { + array, + boolean, + button, + color, + date, + select, + withKnobs, + text, + number, +} from '@storybook/addon-knobs/html'; + +storiesOf('Addons|Knobs', module) + .addDecorator(withKnobs) + .add('Simple', () => { + const name = text('Name', 'John Doe'); + const age = number('Age', 44); + const content = `I am ${name} and I'm ${age} years old.`; + + return `
${content}
`; + }) + .add('All knobs', () => { + const name = text('Name', 'Jane'); + const stock = number('Stock', 20, { + range: true, + min: 0, + max: 30, + step: 5, + }); + const fruits = { + apples: 'Apple', + bananas: 'Banana', + cherries: 'Cherry', + }; + const fruit = select('Fruit', fruits, 'apple'); + const price = number('Price', 2.25); + + const colour = color('Border', 'deeppink'); + const today = date('Today', new Date('Jan 20 2017 GMT+0')); + const items = array('Items', ['Laptop', 'Book', 'Whiskey']); + const nice = boolean('Nice', true); + + const stockMessage = stock + ? `I have a stock of ${stock} ${fruit}, costing $${price} each.` + : `I'm out of ${fruit}${nice ? ', Sorry!' : '.'}`; + + const salutation = nice ? 'Nice to meet you!' : 'Leave me alone!'; + const dateOptions = { year: 'numeric', month: 'long', day: 'numeric', timeZone: 'UTC' }; + + button('Arbitrary action', action('You clicked it!')); + + const style = `border: 2px dotted ${colour}; padding: 8px 22px; border-radius: 8px`; + + return `
+

My name is ${name},

+

today is ${new Date(today).toLocaleDateString('en-US', dateOptions)}

+

${stockMessage}

+

Also, I have:

+
    ${items.map(item => `
  • ${item}
  • `).join('')}
+

${salutation}

+
+ `; + }); diff --git a/examples/html-kitchen-sink/stories/addon-notes.stories.js b/examples/html-kitchen-sink/stories/addon-notes.stories.js new file mode 100644 index 000000000000..a02ff0246d80 --- /dev/null +++ b/examples/html-kitchen-sink/stories/addon-notes.stories.js @@ -0,0 +1,17 @@ +import { storiesOf } from '@storybook/html'; +import { withNotes } from '@storybook/addon-notes'; + +storiesOf('Addons|Notes', module) + .addDecorator(withNotes) + .add( + 'Simple note', + () => + `

+ + This is a fragment of HTML + +

`, + { + notes: 'My notes on some bold text', + } + ); diff --git a/examples/html-kitchen-sink/stories/index.stories.js b/examples/html-kitchen-sink/stories/index.stories.js new file mode 100644 index 000000000000..1dcedbdb3522 --- /dev/null +++ b/examples/html-kitchen-sink/stories/index.stories.js @@ -0,0 +1,24 @@ +import { document } from 'global'; +import { storiesOf } from '@storybook/html'; +import { action } from '@storybook/addon-actions'; +import { withLinks } from '@storybook/addon-links'; + +import './welcome.css'; +import welcome from './welcome.html'; + +storiesOf('Welcome', module) + .addDecorator(withLinks) + .add('Welcome', () => welcome); + +storiesOf('Demo', module) + .add('heading', () => '

Hello World

') + .add( + 'headings', + () => '

Hello World

Hello World

Hello World

Hello World

' + ) + .add('button', () => { + const button = document.createElement('button'); + button.innerHTML = 'Hello Button'; + button.addEventListener('click', action('Click')); + return button; + }); diff --git a/examples/html-kitchen-sink/stories/logo.svg b/examples/html-kitchen-sink/stories/logo.svg new file mode 100644 index 000000000000..0171e29ca41e --- /dev/null +++ b/examples/html-kitchen-sink/stories/logo.svg @@ -0,0 +1,14 @@ + + HTML5 Logo + + + + + + + + + + + + diff --git a/examples/html-kitchen-sink/stories/welcome.css b/examples/html-kitchen-sink/stories/welcome.css new file mode 100644 index 000000000000..ea42a4983159 --- /dev/null +++ b/examples/html-kitchen-sink/stories/welcome.css @@ -0,0 +1,21 @@ +.main { + margin: 15px; + max-width: 600px; + line-height: 1.4; + font-family: "Helvetica Neue", Helvetica, "Segoe UI", Arial, freesans, sans-serif; +} + +.logo { + width: 256px; + margin: 15px; +} + +.code { + font-size: 15px; + font-weight: 600; + padding: 2px 5px; + border: 1px solid #eae9e9; + border-radius: 4px; + background-color: #f3f2f2; + color: #3a3a3a; +} diff --git a/examples/html-kitchen-sink/stories/welcome.html b/examples/html-kitchen-sink/stories/welcome.html new file mode 100644 index 000000000000..af62ae262a1a --- /dev/null +++ b/examples/html-kitchen-sink/stories/welcome.html @@ -0,0 +1,29 @@ +
+

Welcome to Storybook for HTML

+

This is a UI component dev environment for your plain HTML snippets.

+

+ We've added some basic stories inside the stories directory. +
+ A story is a single state of one or more UI components. You can have as many stories as you want. +
+ (Basically a story is like a visual test case.) +

+

+ See these sample stories +

+

+

+ Just like that, you can add your own snippets as stories. +
+ You can also edit those snippets and see changes right away. +
+

+

+ Usually we create stories with smaller UI components in the app.
+ Have a look at the + + Writing Stories + + section in our documentation. +

+
diff --git a/examples/html-kitchen-sink/tests/addon-jest.config.json b/examples/html-kitchen-sink/tests/addon-jest.config.json new file mode 100644 index 000000000000..31526d712636 --- /dev/null +++ b/examples/html-kitchen-sink/tests/addon-jest.config.json @@ -0,0 +1,10 @@ +{ + "rootDir": "../../..", + "verbose": true, + "notify": true, + "transform": { + ".*": "babel-jest" + }, + "setupTestFrameworkScriptFile": "/scripts/jest.init.js", + "testMatch": ["/examples/official-storybook/tests/addon-jest.test.js"] +} diff --git a/examples/html-kitchen-sink/tests/addon-jest.test.js b/examples/html-kitchen-sink/tests/addon-jest.test.js new file mode 100644 index 000000000000..8236a32e390f --- /dev/null +++ b/examples/html-kitchen-sink/tests/addon-jest.test.js @@ -0,0 +1,32 @@ +// This file is excluded from the `/scripts/test.js` script. (see root `jest.config.js` file) +test('true should be true', () => { + expect(true).toBe(true); +}); + +describe('In a describe: ', () => { + test('true should still be true', () => { + expect(true).toBe(true); + }); + + test('a list should contain 3 items', () => { + expect(['a', 'b', '3']).toHaveLength(3); + }); + + test('everything is awesome', () => { + expect('everything is all right').toEqual('everything is awesome'); + }); +}); + +describe('A bunch of failing tests: ', () => { + test('true should still be true', () => { + expect(true).toBe(false); + }); + + test('a list should contain 3 items', () => { + expect(['a', 'b', '3']).toContain(301); + }); + + test('should work', () => { + expect(() => {}).toThrow(); + }); +}); diff --git a/examples/html-kitchen-sink/tests/htmlshots.test.js b/examples/html-kitchen-sink/tests/htmlshots.test.js new file mode 100644 index 000000000000..b682c5c74b24 --- /dev/null +++ b/examples/html-kitchen-sink/tests/htmlshots.test.js @@ -0,0 +1,9 @@ +import path from 'path'; +import initStoryshots, { multiSnapshotWithOptions } from '@storybook/addon-storyshots'; + +initStoryshots({ + framework: 'html', + integrityOptions: { cwd: path.resolve(__dirname, '../stories') }, + configPath: path.resolve(__dirname, '../.storybook'), + test: multiSnapshotWithOptions({}), +}); diff --git a/examples/official-storybook/built-storybooks/html-kitchen-sink b/examples/official-storybook/built-storybooks/html-kitchen-sink new file mode 120000 index 000000000000..09907fc0f899 --- /dev/null +++ b/examples/official-storybook/built-storybooks/html-kitchen-sink @@ -0,0 +1 @@ +../../html-kitchen-sink/storybook-static/ \ No newline at end of file diff --git a/examples/official-storybook/stories/__snapshots__/app-acceptance.stories.storyshot b/examples/official-storybook/stories/__snapshots__/app-acceptance.stories.storyshot index cb80d4613fb5..b21bba9770d8 100644 --- a/examples/official-storybook/stories/__snapshots__/app-acceptance.stories.storyshot +++ b/examples/official-storybook/stories/__snapshots__/app-acceptance.stories.storyshot @@ -16,6 +16,14 @@ exports[`Storyshots App|acceptance cra-kitchen-sink 1`] = ` /> `; +exports[`Storyshots App|acceptance html-kitchen-sink 1`] = ` +