'),
+ }}
+ />
+ ))
+ .add('accepts story parameters', () =>
{text('Rendered string', '
Hello ')}, {
+ knobs: { escapeHTML: false },
+ });
+
+storiesOf('React|Knobs.withKnobs using options', module)
+ .addParameters({ framework: 'react' })
+ .addDecorator(
+ withKnobs({
+ escapeHTML: false,
+ })
+ )
+ .add('accepts options', () =>
{text('Rendered string', '
Hello ')});
diff --git a/examples/html-kitchen-sink/stories/react/addon-links.stories.js b/examples/html-kitchen-sink/stories/react/addon-links.stories.js
new file mode 100644
index 000000000000..b380cd67ba49
--- /dev/null
+++ b/examples/html-kitchen-sink/stories/react/addon-links.stories.js
@@ -0,0 +1,64 @@
+import React, { Fragment } from 'react';
+import { storiesOf } from '@storybook/html';
+import { linkTo, hrefTo } from '@storybook/addon-links';
+import { action } from '@storybook/addon-actions';
+
+import LinkTo from '@storybook/addon-links/react';
+
+storiesOf('React|Links.Link', module)
+ .addParameters({ framework: 'react' })
+ .add('First', () =>
Go to Second )
+ .add('Second', () =>
Go to First );
+
+storiesOf('React|Links.Button', module)
+ .addParameters({ framework: 'react' })
+ .add('First', () => (
+
+ Go to "Second"
+
+ ))
+ .add('Second', () => (
+
+ Go to "First"
+
+ ));
+
+storiesOf('React|Links.Select', module)
+ .addParameters({ framework: 'react' })
+ .add('Index', () => (
+
e.currentTarget.value)}>
+ Index
+ First
+ Second
+ Third
+
+ ))
+ .add('First', () =>
Go back )
+ .add('Second', () =>
Go back )
+ .add('Third', () =>
Go back );
+
+storiesOf('React|Links.Href', module).add(
+ 'log',
+ () => {
+ hrefTo('React|Links.Href', 'log').then(href => action('URL of this story')(href));
+
+ return
See action logger ;
+ },
+ {
+ framework: 'react',
+ options: {
+ panel: 'storybook/actions/panel',
+ },
+ }
+);
+
+storiesOf('React|Links.Scroll position', module)
+ .addParameters({ framework: 'react' })
+ .addDecorator(storyFn => (
+
+ Scroll down to see the link
+ {storyFn()}
+
+ ))
+ .add('First', () =>
Go to Second )
+ .add('Second', () =>
Go to First );
diff --git a/examples/html-kitchen-sink/stories/react/addon-notes.stories.js b/examples/html-kitchen-sink/stories/react/addon-notes.stories.js
new file mode 100644
index 000000000000..8b720fe84bd8
--- /dev/null
+++ b/examples/html-kitchen-sink/stories/react/addon-notes.stories.js
@@ -0,0 +1,67 @@
+import React from 'react';
+import { storiesOf } from '@storybook/html';
+
+import BaseButton from './components/BaseButton';
+import markdownNotes from './notes/notes.md';
+
+const baseStory = () => (
+
+);
+
+const markdownString = `
+# Documentation
+
+This is inline github-flavored markdown!
+
+## Example Usage
+~~~js
+import React from 'react';
+import { storiesOf } from '@storybook/html';
+import markdownNotes from './readme.md';
+
+storiesOf('React|Notes', module)
+ .add(
+ 'addon notes rendering imported markdown',
+ () => (
+
+ ),
+ {
+ notes: markdownNotes,
+ }
+ )
+~~~
+`;
+
+const markdownTable = `
+| Column1 | Column2 | Column3 |
+|---------|---------|---------|
+| Row1.1 | Row1.2 | Row1.3 |
+| Row2.1 | Row2.2 | Row2.3 |
+| Row3.1 | Row3.2 | Row3.3 |
+| Row4.1 | Row4.2 | Row4.3 |
+`;
+
+const giphyMarkdown = `
+# Giphy
+
+
+`;
+
+storiesOf('React|Notes', module)
+ .addParameters({ framework: 'react' })
+ .add('addon notes', baseStory, {
+ notes:
+ 'This is the notes for a button. This is helpful for adding details about a story in a separate panel.',
+ })
+ .add('addon notes rendering imported markdown', baseStory, {
+ notes: { markdown: markdownNotes },
+ })
+ .add('addon notes rendering inline, github-flavored markdown', baseStory, {
+ notes: { markdown: markdownString },
+ })
+ .add('with a markdown table', baseStory, {
+ notes: { markdown: markdownTable },
+ })
+ .add('with a markdown giphy', baseStory, {
+ notes: { markdown: giphyMarkdown },
+ });
diff --git a/examples/html-kitchen-sink/stories/react/addon-options.stories.js b/examples/html-kitchen-sink/stories/react/addon-options.stories.js
new file mode 100644
index 000000000000..bdeef6699c8f
--- /dev/null
+++ b/examples/html-kitchen-sink/stories/react/addon-options.stories.js
@@ -0,0 +1,19 @@
+import React from 'react';
+import { storiesOf } from '@storybook/html';
+
+storiesOf('React|Options', module)
+ .addParameters({ framework: 'react' })
+ .add('setting name', () =>
This story should have changed the name of the storybook
, {
+ options: {
+ name: 'Custom Storybook',
+ },
+ })
+ .add(
+ 'hiding addon panel',
+ () =>
This story should have changed hidden the addons panel
,
+ {
+ options: {
+ showPanel: false,
+ },
+ }
+ );
diff --git a/examples/html-kitchen-sink/stories/react/addon-viewport.stories.js b/examples/html-kitchen-sink/stories/react/addon-viewport.stories.js
new file mode 100644
index 000000000000..7467a5f5b96e
--- /dev/null
+++ b/examples/html-kitchen-sink/stories/react/addon-viewport.stories.js
@@ -0,0 +1,48 @@
+import React from 'react';
+import { storiesOf } from '@storybook/html';
+
+import { styled } from '@storybook/theming';
+
+import { INITIAL_VIEWPORTS } from '@storybook/addon-viewport';
+
+const Panel = styled.div();
+
+storiesOf('React|Viewport', module)
+ .addParameters({ framework: 'react' })
+ .add('default', () => (
+
I don't have problems being rendered using the default viewport.
+ ));
+
+storiesOf('React|Viewport.Custom Default (Kindle Fire 2)', module)
+ .addParameters({ framework: 'react' })
+ .addParameters({
+ viewport: {
+ viewports: {
+ ...INITIAL_VIEWPORTS,
+ kindleFire2: {
+ name: 'Kindle Fire 2',
+ styles: {
+ width: '600px',
+ height: '963px',
+ },
+ },
+ },
+ },
+ })
+ .add('Inherited', () => (
+
+ I've inherited Kindle Fire 2 viewport from my parent.
+
+ ))
+ .add(
+ 'Overridden via "withViewport" parameterized decorator',
+ () => (
+
+ I respect my parents but I should be looking good on iPad .
+
+ ),
+ { viewport: { defaultViewport: 'ipad' } }
+ )
+ .add('Disabled', () =>
There should be no viewport selector in the toolbar , {
+ viewport: { disable: true },
+ });
diff --git a/examples/html-kitchen-sink/stories/react/button.stories.js b/examples/html-kitchen-sink/stories/react/button.stories.js
new file mode 100644
index 000000000000..9980c10f45fe
--- /dev/null
+++ b/examples/html-kitchen-sink/stories/react/button.stories.js
@@ -0,0 +1,20 @@
+import React, { useState } from 'react';
+import Button from './components/BaseButton';
+
+export default {
+ title: 'React|Button',
+ component: Button,
+ parameters: {
+ framework: 'react',
+ },
+};
+
+export const withCounter = () => {
+ const [counter, setCounter] = useState(0);
+ const label = `Testing: ${counter}`;
+ return
setCounter(counter + 1)} label={label} />;
+};
+
+withCounter.story = {
+ name: 'with counter',
+};
diff --git a/examples/html-kitchen-sink/stories/react/components/BaseButton.js b/examples/html-kitchen-sink/stories/react/components/BaseButton.js
new file mode 100644
index 000000000000..17b2ce719fda
--- /dev/null
+++ b/examples/html-kitchen-sink/stories/react/components/BaseButton.js
@@ -0,0 +1,28 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+/** BaseButton component description imported from comments inside the component file */
+const BaseButton = ({ disabled, label, onClick, style }) => (
+
+ {label}
+
+);
+
+BaseButton.defaultProps = {
+ disabled: false,
+ onClick: () => {},
+ style: {},
+};
+
+BaseButton.propTypes = {
+ /** Boolean indicating whether the button should render as disabled */
+ disabled: PropTypes.bool,
+ /** button label. */
+ label: PropTypes.string.isRequired,
+ /** onClick handler */
+ onClick: PropTypes.func,
+ /** Custom styles */
+ style: PropTypes.shape({}),
+};
+
+export default BaseButton;
diff --git a/examples/html-kitchen-sink/stories/react/components/DelayedRender.js b/examples/html-kitchen-sink/stories/react/components/DelayedRender.js
new file mode 100644
index 000000000000..85ec79e1a6af
--- /dev/null
+++ b/examples/html-kitchen-sink/stories/react/components/DelayedRender.js
@@ -0,0 +1,30 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+
+export default class DelayedRender extends Component {
+ static propTypes = {
+ children: PropTypes.node.isRequired,
+ };
+
+ state = {
+ show: false,
+ };
+
+ componentDidMount() {
+ this.showTO = setTimeout(() => {
+ this.setState({
+ show: true,
+ });
+ }, 1000);
+ }
+
+ componentWillUnmount() {
+ clearTimeout(this.showTO);
+ }
+
+ render() {
+ const { show } = this.state;
+ const { children } = this.props;
+ return show ? children :
;
+ }
+}
diff --git a/examples/html-kitchen-sink/stories/react/components/DocgenButton.js b/examples/html-kitchen-sink/stories/react/components/DocgenButton.js
new file mode 100644
index 000000000000..9267c473b11d
--- /dev/null
+++ b/examples/html-kitchen-sink/stories/react/components/DocgenButton.js
@@ -0,0 +1,149 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+/** DocgenButton component description imported from comments inside the component file */
+const DocgenButton = ({ disabled, label, onClick }) => (
+
+ {label}
+
+);
+
+DocgenButton.defaultProps = {
+ disabled: false,
+ onClick: () => {},
+ optionalString: 'Default String',
+ one: { key: 1 },
+ two: {
+ thing: {
+ id: 2,
+ func: () => {},
+ arr: [],
+ },
+ },
+ obj: {
+ key: 'value',
+ },
+ shape: {
+ id: 3,
+ func: () => {},
+ arr: [],
+ shape: {
+ shape: {
+ foo: 'bar',
+ },
+ },
+ },
+ arrayOf: [1, 2, 3],
+ msg: new Set(),
+ enm: 'News',
+ enmEval: 'Photos',
+ union: 'hello',
+};
+
+/* eslint-disable react/no-unused-prop-types */
+
+DocgenButton.propTypes = {
+ /** Boolean indicating whether the button should render as disabled */
+ disabled: PropTypes.bool,
+ /** button label. */
+ label: PropTypes.string.isRequired,
+ /** onClick handler */
+ onClick: PropTypes.func,
+ /**
+ * A simple `objectOf` propType.
+ */
+ one: PropTypes.objectOf(PropTypes.number),
+ /**
+ * A very complex `objectOf` propType.
+ */
+ two: PropTypes.objectOf(
+ PropTypes.shape({
+ /**
+ * Just an internal propType for a shape.
+ * It's also required, and as you can see it supports multi-line comments!
+ */
+ id: PropTypes.number.isRequired,
+ /**
+ * A simple non-required function
+ */
+ func: PropTypes.func,
+ /**
+ * An `arrayOf` shape
+ */
+ arr: PropTypes.arrayOf(
+ PropTypes.shape({
+ /**
+ * 5-level deep propType definition and still works.
+ */
+ index: PropTypes.number.isRequired,
+ })
+ ),
+ })
+ ),
+
+ /**
+ * Plain object propType (use shape!!)
+ */
+ obj: PropTypes.object, // eslint-disable-line react/forbid-prop-types
+
+ /**
+ * propType for shape with nested arrayOf
+ *
+ * Also, multi-line description
+ */
+ shape: PropTypes.shape({
+ /**
+ * Just an internal propType for a shape.
+ * It's also required, and as you can see it supports multi-line comments!
+ */
+ id: PropTypes.number.isRequired,
+ /**
+ * A simple non-required function
+ */
+ func: PropTypes.func,
+ /**
+ * An `arrayOf` shape
+ */
+ arr: PropTypes.arrayOf(
+ PropTypes.shape({
+ /**
+ * 5-level deep propType definition and still works.
+ */
+ index: PropTypes.number.isRequired,
+ })
+ ),
+
+ shape: PropTypes.shape({
+ shape: PropTypes.shape({
+ foo: PropTypes.string,
+ }),
+ }),
+ }),
+
+ /**
+ * array of a certain type
+ */
+ arrayOf: PropTypes.arrayOf(PropTypes.number),
+
+ /**
+ * `instanceOf` is also supported and the custom type will be shown instead of `instanceOf`
+ */
+ msg: PropTypes.instanceOf(Set),
+ /**
+ * `oneOf` is basically an Enum which is also supported but can be pretty big.
+ */
+ enm: PropTypes.oneOf(['News', 'Photos']),
+ enmEval: PropTypes.oneOf((() => ['News', 'Photos'])()),
+ /**
+ * A multi-type prop is also valid and is displayed as `Union`
+ */
+ union: PropTypes.oneOfType([PropTypes.string, PropTypes.instanceOf(Set)]),
+ /**
+ * test string with a comment that has
+ * two identical lines
+ * two identical lines
+ */
+ optionalString: PropTypes.string,
+};
+
+export default DocgenButton;
diff --git a/examples/html-kitchen-sink/stories/react/components/FlowTypeButton.js b/examples/html-kitchen-sink/stories/react/components/FlowTypeButton.js
new file mode 100644
index 000000000000..5359320772e3
--- /dev/null
+++ b/examples/html-kitchen-sink/stories/react/components/FlowTypeButton.js
@@ -0,0 +1,50 @@
+// @flow
+import React from 'react';
+
+const Message = {};
+
+interface PropsType {
+ /** A multi-type prop to be rendered in the button */
+ label: string;
+ /** Function to be called when the button is clicked */
+ onClick?: Function;
+ /** Boolean representing whether the button is disabled */
+ disabled?: boolean;
+ /** A plain object */
+ obj?: Record;
+ /** A complex Object with nested types */
+ shape: {
+ id: number,
+ func?: Function,
+ arr?: {
+ index: number,
+ }[],
+ shape?: {
+ shape?: {
+ foo?: string,
+ },
+ },
+ };
+ /** An array of numbers */
+ arrayOf?: number[];
+ /** A custom type */
+ msg?: typeof Message;
+ /** `oneOf` as Enum */
+ enm?: 'News' | 'Photos';
+ /** `oneOf` A multi-type prop of string or custom Message */
+ union?: string | typeof Message;
+}
+
+/** FlowTypeButton component description imported from comments inside the component file */
+const FlowTypeButton = ({ label, onClick, disabled }: PropsType) => (
+
+ {label}
+
+);
+
+FlowTypeButton.defaultProps = {
+ disabled: false,
+ onClick: () => {},
+};
+
+export default FlowTypeButton;
diff --git a/examples/html-kitchen-sink/stories/react/components/ForwardedRefButton.js b/examples/html-kitchen-sink/stories/react/components/ForwardedRefButton.js
new file mode 100644
index 000000000000..72f69de58bf8
--- /dev/null
+++ b/examples/html-kitchen-sink/stories/react/components/ForwardedRefButton.js
@@ -0,0 +1,24 @@
+import React, { forwardRef } from 'react';
+import PropTypes from 'prop-types';
+import BaseButton from './BaseButton';
+
+const ForwardedRefButton = forwardRef((props, ref) => );
+
+ForwardedRefButton.defaultProps = {
+ disabled: false,
+ onClick: () => {},
+ style: {},
+};
+
+ForwardedRefButton.propTypes = {
+ /** Boolean indicating whether the button should render as disabled */
+ disabled: PropTypes.bool,
+ /** button label. */
+ label: PropTypes.string.isRequired,
+ /** onClick handler */
+ onClick: PropTypes.func,
+ /** Custom styles */
+ style: PropTypes.shape({}),
+};
+
+export default ForwardedRefButton;
diff --git a/examples/html-kitchen-sink/stories/react/components/ForwardedRefButtonWDisplayName.js b/examples/html-kitchen-sink/stories/react/components/ForwardedRefButtonWDisplayName.js
new file mode 100644
index 000000000000..cbd36e0d3407
--- /dev/null
+++ b/examples/html-kitchen-sink/stories/react/components/ForwardedRefButtonWDisplayName.js
@@ -0,0 +1,28 @@
+import React, { forwardRef } from 'react';
+import PropTypes from 'prop-types';
+import BaseButton from './BaseButton';
+
+const ForwardedRefButtonWDisplayName = forwardRef((props, ref) => (
+
+));
+
+ForwardedRefButtonWDisplayName.defaultProps = {
+ disabled: false,
+ onClick: () => {},
+ style: {},
+};
+
+ForwardedRefButtonWDisplayName.propTypes = {
+ /** Boolean indicating whether the button should render as disabled */
+ disabled: PropTypes.bool,
+ /** button label. */
+ label: PropTypes.string.isRequired,
+ /** onClick handler */
+ onClick: PropTypes.func,
+ /** Custom styles */
+ style: PropTypes.shape({}),
+};
+
+ForwardedRefButtonWDisplayName.displayName = 'ButtonDisplayName';
+
+export default ForwardedRefButtonWDisplayName;
diff --git a/examples/html-kitchen-sink/stories/react/components/ImportedPropsButton.js b/examples/html-kitchen-sink/stories/react/components/ImportedPropsButton.js
new file mode 100644
index 000000000000..a394f33a777d
--- /dev/null
+++ b/examples/html-kitchen-sink/stories/react/components/ImportedPropsButton.js
@@ -0,0 +1,15 @@
+import React from 'react';
+import DocgenButton from './DocgenButton';
+
+/** Button component description */
+const ImportedPropsButton = ({ disabled, label, onClick }) => (
+
+ {label}
+
+);
+
+ImportedPropsButton.defaultProps = DocgenButton.defaultProps;
+
+ImportedPropsButton.propTypes = DocgenButton.propTypes;
+
+export default ImportedPropsButton;
diff --git a/examples/html-kitchen-sink/stories/react/components/NamedExportButton.js b/examples/html-kitchen-sink/stories/react/components/NamedExportButton.js
new file mode 100644
index 000000000000..354bb42fc99b
--- /dev/null
+++ b/examples/html-kitchen-sink/stories/react/components/NamedExportButton.js
@@ -0,0 +1,24 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import BaseButton from './BaseButton';
+
+const NamedExportButton = props => ;
+
+NamedExportButton.defaultProps = {
+ disabled: false,
+ onClick: () => {},
+ style: {},
+};
+
+NamedExportButton.propTypes = {
+ /** Boolean indicating whether the button should render as disabled */
+ disabled: PropTypes.bool,
+ /** button label. */
+ label: PropTypes.string.isRequired,
+ /** onClick handler */
+ onClick: PropTypes.func,
+ /** Custom styles */
+ style: PropTypes.shape({}),
+};
+
+export { NamedExportButton };
diff --git a/examples/html-kitchen-sink/stories/react/components/TableComponent.js b/examples/html-kitchen-sink/stories/react/components/TableComponent.js
new file mode 100644
index 000000000000..d963e0a1f517
--- /dev/null
+++ b/examples/html-kitchen-sink/stories/react/components/TableComponent.js
@@ -0,0 +1,51 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+const Red = props => ;
+const TableComponent = ({ propDefinitions }) => {
+ const props = propDefinitions.map(
+ ({ property, propType, required, description, defaultValue }) => (
+
+
+ {property}
+ {required ? * : null}
+
+ {propType.name}
+ {JSON.stringify(defaultValue)}
+ {description}
+
+ )
+ );
+
+ return (
+
+
+
+ name
+ type
+ default
+ description
+
+
+ {props}
+
+ );
+};
+
+TableComponent.defaultProps = {
+ propDefinitions: [],
+};
+
+TableComponent.propTypes = {
+ propDefinitions: PropTypes.arrayOf(
+ PropTypes.shape({
+ property: PropTypes.string.isRequired,
+ propType: PropTypes.oneOfType([PropTypes.object, PropTypes.string]).isRequired,
+ required: PropTypes.bool.isRequired,
+ description: PropTypes.string,
+ defaultValue: PropTypes.any,
+ })
+ ),
+};
+
+export default TableComponent;
diff --git a/examples/html-kitchen-sink/stories/react/components/addon-a11y/Button.js b/examples/html-kitchen-sink/stories/react/components/addon-a11y/Button.js
new file mode 100644
index 000000000000..293f6ca97303
--- /dev/null
+++ b/examples/html-kitchen-sink/stories/react/components/addon-a11y/Button.js
@@ -0,0 +1,48 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+const styles = {
+ button: {
+ padding: '12px 6px',
+ fontSize: '12px',
+ lineHeight: '16px',
+ borderRadius: '5px',
+ },
+ ok: {
+ backgroundColor: '#028402',
+ color: '#ffffff',
+ },
+ wrong: {
+ color: '#ffffff',
+ backgroundColor: '#4caf50',
+ },
+};
+
+function Button({ content, disabled, contrast }) {
+ return (
+
+ {content}
+
+ );
+}
+
+Button.propTypes = {
+ content: PropTypes.string,
+ disabled: PropTypes.bool,
+ contrast: PropTypes.oneOf(['ok', 'wrong']),
+};
+
+Button.defaultProps = {
+ content: 'null',
+ disabled: false,
+ contrast: 'ok',
+};
+
+export default Button;
diff --git a/examples/html-kitchen-sink/stories/react/components/addon-a11y/Form/Input.js b/examples/html-kitchen-sink/stories/react/components/addon-a11y/Form/Input.js
new file mode 100644
index 000000000000..f5fca29664d8
--- /dev/null
+++ b/examples/html-kitchen-sink/stories/react/components/addon-a11y/Form/Input.js
@@ -0,0 +1,22 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+function Input({ id, value, type, placeholder }) {
+ return ;
+}
+
+Input.propTypes = {
+ type: PropTypes.oneOf(['text', 'password']),
+ id: PropTypes.string,
+ value: PropTypes.string,
+ placeholder: PropTypes.string,
+};
+
+Input.defaultProps = {
+ type: null,
+ id: null,
+ value: null,
+ placeholder: null,
+};
+
+export default Input;
diff --git a/examples/html-kitchen-sink/stories/react/components/addon-a11y/Form/Label.js b/examples/html-kitchen-sink/stories/react/components/addon-a11y/Form/Label.js
new file mode 100644
index 000000000000..5f3799a6ac6c
--- /dev/null
+++ b/examples/html-kitchen-sink/stories/react/components/addon-a11y/Form/Label.js
@@ -0,0 +1,23 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+const styles = {
+ label: {
+ padding: '0 6px',
+ },
+};
+
+function Label({ id, content }) {
+ return (
+
+ {content}
+
+ );
+}
+
+Label.propTypes = {
+ content: PropTypes.string.isRequired,
+ id: PropTypes.string.isRequired,
+};
+
+export default Label;
diff --git a/examples/html-kitchen-sink/stories/react/components/addon-a11y/Form/Row.js b/examples/html-kitchen-sink/stories/react/components/addon-a11y/Form/Row.js
new file mode 100644
index 000000000000..51f87b040529
--- /dev/null
+++ b/examples/html-kitchen-sink/stories/react/components/addon-a11y/Form/Row.js
@@ -0,0 +1,25 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import Label from './Label';
+import Input from './Input';
+
+function Row({ label, input }) {
+ return (
+
+ {label}
+ {input}
+
+ );
+}
+
+Row.propTypes = {
+ label: PropTypes.shape({ type: PropTypes.oneOf([Label]) }),
+ input: PropTypes.shape({ type: PropTypes.oneOf([Input]) }).isRequired,
+};
+
+Row.defaultProps = {
+ label: null,
+};
+
+export default Row;
diff --git a/examples/html-kitchen-sink/stories/react/components/addon-a11y/Form/index.js b/examples/html-kitchen-sink/stories/react/components/addon-a11y/Form/index.js
new file mode 100644
index 000000000000..c893c0a55dd6
--- /dev/null
+++ b/examples/html-kitchen-sink/stories/react/components/addon-a11y/Form/index.js
@@ -0,0 +1,5 @@
+import Input from './Input';
+import Label from './Label';
+import Row from './Row';
+
+export { Input, Label, Row };
diff --git a/examples/html-kitchen-sink/stories/react/errors.stories.js b/examples/html-kitchen-sink/stories/react/errors.stories.js
new file mode 100644
index 000000000000..72a605b40482
--- /dev/null
+++ b/examples/html-kitchen-sink/stories/react/errors.stories.js
@@ -0,0 +1,47 @@
+import React, { Fragment } from 'react';
+
+const badOutput = { renderable: 'no, react can not render objects' };
+const BadComponent = () => badOutput;
+
+export default {
+ title: 'React|Errors',
+ parameters: {
+ framework: 'react',
+ },
+};
+
+export const exception = () => {
+ throw new Error('storyFn threw an error! WHOOPS');
+};
+exception.story = {
+ name: 'story throws exception',
+ parameters: {
+ storyshots: { disable: true },
+ chromatic: { disable: true },
+ },
+};
+
+export const badComponent = () => (
+
+ Hello world
+
+
+);
+badComponent.story = {
+ name: 'story errors - invariant error',
+ parameters: {
+ notes: 'Story does not return something react can render',
+ storyshots: { disable: true },
+ chromatic: { disable: true },
+ },
+};
+
+export const badStory = () => badOutput;
+badStory.story = {
+ name: 'story errors - story un-renderable type',
+ parameters: {
+ notes: 'Story does not return something react can render',
+ storyshots: { disable: true },
+ chromatic: { disable: true },
+ },
+};
diff --git a/examples/html-kitchen-sink/stories/react/notes/notes.md b/examples/html-kitchen-sink/stories/react/notes/notes.md
new file mode 100644
index 000000000000..1d2e3472f55c
--- /dev/null
+++ b/examples/html-kitchen-sink/stories/react/notes/notes.md
@@ -0,0 +1,11 @@
+# This is a Markdown File
+
+#### It is imported and compiled using a webpack markdown loader
+
+Supports code snippets too:
+
+```jsx
+
+ Foo
+
+```
diff --git a/examples/html-kitchen-sink/tests/htmlshots.test.js b/examples/html-kitchen-sink/tests/htmlshots.test.js
index f7d22dfc52ab..20195de00142 100644
--- a/examples/html-kitchen-sink/tests/htmlshots.test.js
+++ b/examples/html-kitchen-sink/tests/htmlshots.test.js
@@ -3,6 +3,7 @@ import initStoryshots, { multiSnapshotWithOptions } from '@storybook/addon-story
initStoryshots({
framework: 'html',
+ storyKindRegex: /^(?!React).+/,
integrityOptions: { cwd: path.resolve(__dirname, '../stories') },
configPath: path.resolve(__dirname, '../.storybook'),
test: multiSnapshotWithOptions(),
diff --git a/jest.config.js b/jest.config.js
index 99816f51e250..4a668e925bd2 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -61,6 +61,7 @@ module.exports = {
'app/**/*.{js,jsx,ts,tsx}',
'lib/**/*.{js,jsx,ts,tsx}',
'addons/**/*.{js,jsx,ts,tsx}',
+ 'renderers/**/*.{js,jsx,ts,tsx}',
],
coveragePathIgnorePatterns: [
'/node_modules/',
diff --git a/lib/addons/src/hooks.ts b/lib/addons/src/hooks.ts
index a7750250d139..1ec2aabb99ff 100644
--- a/lib/addons/src/hooks.ts
+++ b/lib/addons/src/hooks.ts
@@ -127,7 +127,7 @@ export class HooksContext {
}
}
-const hookify = (fn: AbstractFunction) => (...args: any[]) => {
+export const hookify = (fn: AbstractFunction) => (...args: any[]) => {
const { hooks }: StoryContext = typeof args[0] === 'function' ? args[1] : args[0];
const prevPhase = hooks.currentPhase;
diff --git a/lib/addons/src/types.ts b/lib/addons/src/types.ts
index 35b7b2ba6981..2b3a6d9714c2 100644
--- a/lib/addons/src/types.ts
+++ b/lib/addons/src/types.ts
@@ -32,9 +32,7 @@ export interface StoryContext {
export interface WrapperSettings {
options: OptionsParameter;
- parameters: {
- [key: string]: any;
- };
+ parameters: any;
}
export interface OptionsParameter extends Object {
diff --git a/lib/client-api/src/client_api.ts b/lib/client-api/src/client_api.ts
index 4e02fb8c99c9..d6cae5b187fd 100644
--- a/lib/client-api/src/client_api.ts
+++ b/lib/client-api/src/client_api.ts
@@ -2,7 +2,14 @@
import deprecate from 'util-deprecate';
import isPlainObject from 'is-plain-object';
import { logger } from '@storybook/client-logger';
-import addons, { StoryContext, StoryFn, Parameters, OptionsParameter } from '@storybook/addons';
+import addons, {
+ makeDecorator,
+ StoryContext,
+ StoryGetter,
+ StoryFn,
+ Parameters,
+ OptionsParameter,
+} from '@storybook/addons';
import Events from '@storybook/core-events';
import { toId } from '@storybook/router/utils';
@@ -11,7 +18,7 @@ import isEqual from 'lodash/isEqual';
import get from 'lodash/get';
import { ClientApiParams, DecoratorFunction, ClientApiAddons, StoryApi } from './types';
import subscriptionsStore from './subscriptions_store';
-import { applyHooks } from './hooks';
+import { applyHooks, hookify } from './hooks';
import StoryStore from './story_store';
// merge with concatenating arrays, but no duplicates
@@ -82,6 +89,34 @@ const withSubscriptionTracking = (storyFn: StoryFn) => {
return result;
};
+type Renderer = (getStory: StoryGetter, context: StoryContext) => R;
+interface RendererConfig {
+ framework: string;
+ inner?: Renderer;
+ outer?: Renderer> | null;
+}
+const renderers = {
+ inner: new Map(),
+ outer: new Map(),
+};
+export const registerRenderer = ({ framework, inner, outer }: RendererConfig) => {
+ if (inner != null) {
+ renderers.inner.set(framework, hookify(inner));
+ }
+ if (outer != null) {
+ renderers.outer.set(framework, hookify(outer));
+ }
+};
+const withRenderer = (key: 'inner' | 'outer') =>
+ makeDecorator({
+ name: 'withReact',
+ parameterName: 'framework',
+ wrapper: (getStory, context, { parameters: framework }) =>
+ renderers[key].has(framework)
+ ? renderers[key].get(framework)(getStory, context)
+ : getStory(context),
+ });
+
export default class ClientApi {
private _storyStore: StoryStore;
@@ -242,10 +277,12 @@ export default class ClientApi {
{
applyDecorators: applyHooks(this._decorateStory),
getDecorators: () => [
+ withRenderer('inner'),
...(allParam.decorators || []),
...localDecorators,
..._globalDecorators,
withSubscriptionTracking,
+ withRenderer('outer'),
],
}
);
diff --git a/lib/client-api/src/hooks.ts b/lib/client-api/src/hooks.ts
index 3017db5d2af4..0b944d38d0e2 100644
--- a/lib/client-api/src/hooks.ts
+++ b/lib/client-api/src/hooks.ts
@@ -1,6 +1,7 @@
import {
HooksContext,
applyHooks,
+ hookify,
useMemo,
useCallback,
useRef,
@@ -15,6 +16,7 @@ import {
export {
HooksContext,
applyHooks,
+ hookify,
useMemo,
useCallback,
useRef,
diff --git a/lib/client-api/src/index.ts b/lib/client-api/src/index.ts
index 575cccde1e91..66dde51a1cfe 100644
--- a/lib/client-api/src/index.ts
+++ b/lib/client-api/src/index.ts
@@ -1,4 +1,4 @@
-import ClientApi, { defaultDecorateStory } from './client_api';
+import ClientApi, { defaultDecorateStory, registerRenderer } from './client_api';
import StoryStore from './story_store';
import ConfigApi from './config_api';
import subscriptionsStore from './subscriptions_store';
@@ -17,4 +17,5 @@ export {
pathToId,
getQueryParams,
getQueryParam,
+ registerRenderer,
};
diff --git a/lib/core/src/client/preview/start.js b/lib/core/src/client/preview/start.js
index 8099ea3eb223..98b25d315666 100644
--- a/lib/core/src/client/preview/start.js
+++ b/lib/core/src/client/preview/start.js
@@ -52,9 +52,9 @@ function showNopreview() {
document.body.classList.add(classes.NOPREVIEW);
}
-function showErrorDisplay({ message = '', stack = '' }) {
+function showErrorDisplay({ message = '', stack = '', description = stack }) {
document.getElementById('error-message').innerHTML = ansiConverter.toHtml(message);
- document.getElementById('error-stack').innerHTML = ansiConverter.toHtml(stack);
+ document.getElementById('error-stack').innerHTML = ansiConverter.toHtml(description);
document.body.classList.remove(classes.MAIN);
document.body.classList.remove(classes.NOPREVIEW);
@@ -68,7 +68,7 @@ function showError({ title, description }) {
addons.getChannel().emit(Events.STORY_ERRORED, { title, description });
showErrorDisplay({
message: title,
- stack: description,
+ description,
});
}
diff --git a/package.json b/package.json
index 7323b1609f39..58402dbd3499 100644
--- a/package.json
+++ b/package.json
@@ -37,7 +37,8 @@
"examples-native/*",
"examples/*",
"lib/*",
- "lib/cli/test/run/*"
+ "lib/cli/test/run/*",
+ "renderers/*"
],
"scripts": {
"bootstrap": "node ./scripts/bootstrap.js",
diff --git a/renderers/react/README.md b/renderers/react/README.md
new file mode 100644
index 000000000000..77c0aad88f84
--- /dev/null
+++ b/renderers/react/README.md
@@ -0,0 +1,35 @@
+# Storybook Renderer React
+
+React renderer for Storybook can be used for declaring React stories in `@storybook/html`.
+
+## Getting Started
+
+Install:
+
+```sh
+npm i -D @storybook/renderer-react
+```
+
+Then, add following content to `.storybook/config.js`
+
+```js
+import {addDecorator} from '@storybook/html'
+import withReact from '@storybook/renderer-react'
+
+addDecorator(withReact)
+```
+
+Then, you can add `{framework: 'react'}` parameter to mark React stories:
+
+```js
+import React from 'react';
+import { storiesOf } from '@storybook/html';
+
+import Button from './button';
+
+storiesOf('Button', module)
+ .addParameters({framework: 'react'})
+ .add('default view', () => (
+ Hello World!
+ ));
+```
diff --git a/renderers/react/package.json b/renderers/react/package.json
new file mode 100644
index 000000000000..ff92bab54fe0
--- /dev/null
+++ b/renderers/react/package.json
@@ -0,0 +1,43 @@
+{
+ "name": "@storybook/renderer-react",
+ "version": "5.3.0-alpha.17",
+ "description": "React renderer for Storybook",
+ "keywords": [
+ "react",
+ "storybook"
+ ],
+ "homepage": "https://github.com/storybooks/storybook/tree/master/renderers/react",
+ "bugs": {
+ "url": "https://github.com/storybooks/storybook/issues"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/storybooks/storybook.git",
+ "directory": "app/react"
+ },
+ "license": "MIT",
+ "main": "dist/client/index.js",
+ "types": "dist/client/index.d.ts",
+ "scripts": {
+ "prepare": "node ../../scripts/prepare.js"
+ },
+ "dependencies": {
+ "@babel/plugin-transform-react-constant-elements": "^7.2.0",
+ "@babel/preset-flow": "^7.0.0",
+ "@babel/preset-react": "^7.0.0",
+ "@storybook/addons": "5.3.0-alpha.17",
+ "@storybook/client-api": "5.3.0-alpha.17",
+ "babel-plugin-add-react-displayname": "^0.0.5",
+ "babel-plugin-react-docgen": "^3.1.0",
+ "common-tags": "^1.8.0",
+ "global": "^4.3.2"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.1",
+ "react": "*",
+ "react-dom": "*"
+ },
+ "publishConfig": {
+ "access": "public"
+ }
+}
diff --git a/renderers/react/register.js b/renderers/react/register.js
new file mode 100644
index 000000000000..c73cc796adc5
--- /dev/null
+++ b/renderers/react/register.js
@@ -0,0 +1 @@
+require('.').register();
diff --git a/renderers/react/src/client/globals.ts b/renderers/react/src/client/globals.ts
new file mode 100644
index 000000000000..32f6210bea33
--- /dev/null
+++ b/renderers/react/src/client/globals.ts
@@ -0,0 +1,10 @@
+import { window } from 'global';
+
+if (window && window.parent !== window) {
+ try {
+ // eslint-disable-next-line no-underscore-dangle
+ window.__REACT_DEVTOOLS_GLOBAL_HOOK__ = window.parent.__REACT_DEVTOOLS_GLOBAL_HOOK__;
+ } catch (error) {
+ // The above line can throw if we do not have access to the parent frame -- i.e. cross origin
+ }
+}
diff --git a/renderers/react/src/client/index.tsx b/renderers/react/src/client/index.tsx
new file mode 100644
index 000000000000..80e7f3388303
--- /dev/null
+++ b/renderers/react/src/client/index.tsx
@@ -0,0 +1,23 @@
+import ReactDOM from 'react-dom';
+import React from 'react';
+import { registerRenderer, useEffect, useMemo } from '@storybook/client-api';
+import { document } from 'global';
+
+import './globals';
+import render from './render';
+
+export const register = (skipOuter?: boolean) =>
+ registerRenderer({
+ framework: 'react',
+ inner: (Story, context) => ,
+ outer: skipOuter
+ ? null
+ : async (getStory, context) => {
+ const node = useMemo(() => document.createElement('div'), [context.kind, context.name]);
+ useEffect(() => () => ReactDOM.unmountComponentAtNode(node), [node]);
+ await render(getStory(context), node, context);
+ return node;
+ },
+ });
+
+export { render };
diff --git a/renderers/react/src/client/register.ts b/renderers/react/src/client/register.ts
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/renderers/react/src/client/render.tsx b/renderers/react/src/client/render.tsx
new file mode 100644
index 000000000000..fd23fb0340b9
--- /dev/null
+++ b/renderers/react/src/client/render.tsx
@@ -0,0 +1,52 @@
+import React, { Component, ReactNode } from 'react';
+import ReactDOM from 'react-dom';
+import { StoryContext } from '@storybook/addons';
+
+class ErrorBoundary extends Component<{
+ resolve: () => void;
+ reject: (err: Error) => void;
+}> {
+ state = { hasError: false };
+
+ static getDerivedStateFromError() {
+ return { hasError: true };
+ }
+
+ componentDidMount() {
+ this.resolveIfNoError();
+ }
+
+ componentDidUpdate() {
+ this.resolveIfNoError();
+ }
+
+ componentDidCatch(err: Error) {
+ const { reject } = this.props;
+ reject(err);
+ }
+
+ resolveIfNoError() {
+ const { hasError } = this.state;
+ const { resolve } = this.props;
+ if (!hasError) {
+ resolve();
+ }
+ }
+
+ render() {
+ const { hasError } = this.state;
+ const { children } = this.props;
+
+ return hasError ? null : children;
+ }
+}
+
+export default (element: ReactNode, container: Element, context: Partial) =>
+ new Promise((resolve, reject) => {
+ ReactDOM.render(
+
+ {element}
+ ,
+ container
+ );
+ });
diff --git a/app/react/src/server/framework-preset-react-docgen.test.ts b/renderers/react/src/server/framework-preset-react-docgen.test.ts
similarity index 93%
rename from app/react/src/server/framework-preset-react-docgen.test.ts
rename to renderers/react/src/server/framework-preset-react-docgen.test.ts
index 1b1f0e27ca87..359f27689de9 100644
--- a/app/react/src/server/framework-preset-react-docgen.test.ts
+++ b/renderers/react/src/server/framework-preset-react-docgen.test.ts
@@ -1,4 +1,3 @@
-import { TransformOptions } from '@babel/core';
import * as preset from './framework-preset-react-docgen';
describe('framework-preset-react-docgen', () => {
@@ -29,10 +28,10 @@ describe('framework-preset-react-docgen', () => {
});
it('should return the config with the extra plugins when `plugins` is not an array.', () => {
- const babelConfig: TransformOptions = {
+ const babelConfig = {
babelrc: false,
presets: ['env', 'foo-preset'],
- plugins: ['bar-plugin'],
+ plugins: 'bar-plugin',
};
const config = preset.babel(babelConfig);
diff --git a/app/react/src/server/framework-preset-react-docgen.ts b/renderers/react/src/server/framework-preset-react-docgen.ts
similarity index 100%
rename from app/react/src/server/framework-preset-react-docgen.ts
rename to renderers/react/src/server/framework-preset-react-docgen.ts
diff --git a/app/react/src/server/framework-preset-react.ts b/renderers/react/src/server/framework-preset-react.ts
similarity index 100%
rename from app/react/src/server/framework-preset-react.ts
rename to renderers/react/src/server/framework-preset-react.ts
diff --git a/renderers/react/src/typings.d.ts b/renderers/react/src/typings.d.ts
new file mode 100644
index 000000000000..2f4eb9cf4fd9
--- /dev/null
+++ b/renderers/react/src/typings.d.ts
@@ -0,0 +1 @@
+declare module 'global';
diff --git a/renderers/react/tsconfig.json b/renderers/react/tsconfig.json
new file mode 100644
index 000000000000..95550e38f57f
--- /dev/null
+++ b/renderers/react/tsconfig.json
@@ -0,0 +1,13 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "rootDir": "./src",
+ "types": ["webpack-env"]
+ },
+ "include": [
+ "src/**/*"
+ ],
+ "exclude": [
+ "src/**/*.test.*"
+ ]
+}
diff --git a/scripts/test.js b/scripts/test.js
index 60f58c62c232..246601b1bc4d 100644
--- a/scripts/test.js
+++ b/scripts/test.js
@@ -1,4 +1,5 @@
#!/usr/bin/env node
+
const inquirer = require('inquirer');
const program = require('commander');
const childProcess = require('child_process');
diff --git a/yarn.lock b/yarn.lock
index 4becb4eccdf3..63e16393ebc0 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6062,7 +6062,7 @@ babel-plugin-named-asset-import@^0.3.1, babel-plugin-named-asset-import@^0.3.2,
resolved "https://registry.yarnpkg.com/babel-plugin-named-asset-import/-/babel-plugin-named-asset-import-0.3.4.tgz#4a8fc30e9a3e2b1f5ed36883386ab2d84e1089bd"
integrity sha512-S6d+tEzc5Af1tKIMbsf2QirCcPdQ+mKUCY2H1nJj1DyA1ShwpsoxEOAwbWsG5gcXNV/olpvQd9vrUWRx4bnhpw==
-babel-plugin-react-docgen@^3.0.0:
+babel-plugin-react-docgen@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/babel-plugin-react-docgen/-/babel-plugin-react-docgen-3.1.0.tgz#14b02b363a38cc9e08c871df16960d27ef92030f"
integrity sha512-W6xqZnZIWjZuE9IjP7XolxxgFGB5Y9GZk4cLPSWKa10MrT86q7bX4ke9jbrNhFVIRhbmzL8wE1Sn++mIWoJLbw==
@@ -24403,6 +24403,16 @@ react-dom@^16.8.3, react-dom@^16.8.4:
prop-types "^15.6.2"
scheduler "^0.16.1"
+react-dom@^16.8.6:
+ version "16.8.6"
+ resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.8.6.tgz#71d6303f631e8b0097f56165ef608f051ff6e10f"
+ integrity sha512-1nL7PIq9LTL3fthPqwkvr2zY7phIPjYrT0jp4HjyEQrEROnw4dG41VVwi/wfoCneoleqrNX7iAD+pXebJZwrwA==
+ dependencies:
+ loose-envify "^1.1.0"
+ object-assign "^4.1.1"
+ prop-types "^15.6.2"
+ scheduler "^0.13.6"
+
react-draggable@^4.0.3:
version "4.0.3"
resolved "https://registry.yarnpkg.com/react-draggable/-/react-draggable-4.0.3.tgz#6b9f76f66431c47b9070e9b805bbc520df8ca481"
@@ -25002,6 +25012,16 @@ react@^15.4.2:
object-assign "^4.1.0"
prop-types "^15.5.10"
+react@^16.8.6:
+ version "16.8.6"
+ resolved "https://registry.yarnpkg.com/react/-/react-16.8.6.tgz#ad6c3a9614fd3a4e9ef51117f54d888da01f2bbe"
+ integrity sha512-pC0uMkhLaHm11ZSJULfOBqV4tIZkx87ZLvbbQYunNixAAvjnC+snJCg0XQXn9VIsttVsbZP/H/ewzgsd5fxKXw==
+ dependencies:
+ loose-envify "^1.1.0"
+ object-assign "^4.1.1"
+ prop-types "^15.6.2"
+ scheduler "^0.13.6"
+
reactcss@^1.2.0:
version "1.2.3"
resolved "https://registry.yarnpkg.com/reactcss/-/reactcss-1.2.3.tgz#c00013875e557b1cf0dfd9a368a1c3dab3b548dd"
@@ -26574,6 +26594,14 @@ schedule@^0.5.0:
dependencies:
object-assign "^4.1.1"
+scheduler@^0.13.6:
+ version "0.13.6"
+ resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.13.6.tgz#466a4ec332467b31a91b9bf74e5347072e4cd889"
+ integrity sha512-IWnObHt413ucAYKsD9J1QShUKkbKLQQHdxRyw73sw4FN26iWr3DY/H34xGPe4nmL1DwXyWmSWmMrA9TfQbE/XQ==
+ dependencies:
+ loose-envify "^1.1.0"
+ object-assign "^4.1.1"
+
scheduler@^0.16.1:
version "0.16.1"
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.16.1.tgz#a6fb6ddec12dc2119176e6eb54ecfe69a9eba8df"