@kadira/react-storybook-addon-info
Version:
A React Storybook addon to show additional information for your stories.
338 lines (305 loc) • 7.51 kB
JavaScript
import React from 'react';
import MTRC from 'markdown-to-react-components';
import PropTable from './PropTable';
import Node from './Node';
import { baseFonts } from './theme';
import { Pre } from './markdown';
const stylesheet = {
link: {
base: {
fontFamily: 'sans-serif',
fontSize: '12px',
display: 'block',
position: 'fixed',
textDecoration: 'none',
background: '#28c',
color: '#fff',
padding: '5px 15px',
cursor: 'pointer',
},
topRight: {
top: 0,
right: 0,
borderRadius: '0 0 0 5px',
},
},
info: {
position: 'absolute',
background: 'white',
top: 0,
bottom: 0,
left: 0,
right: 0,
padding: '0 40px',
overflow: 'auto',
},
children: {
position: 'relative',
zIndex: 0,
},
infoBody: {
...baseFonts,
fontWeight: 300,
lineHeight: 1.45,
fontSize: '15px',
},
infoContent: {
marginBottom: 0,
},
jsxInfoContent: {
borderTop: '1px solid #eee',
margin: '20px 0 0 0',
},
header: {
h1: {
margin: 0,
padding: 0,
fontSize: '35px',
},
h2: {
margin: '0 0 10px 0',
padding: 0,
fontWeight: 400,
fontSize: '22px',
},
body: {
borderBottom: '1px solid #eee',
paddingTop: 10,
marginBottom: 10,
},
},
source: {
h1: {
margin: '20px 0 0 0',
padding: '0 0 5px 0',
fontSize: '25px',
borderBottom: '1px solid #EEE',
},
},
propTableHead: {
margin: '20px 0 0 0',
},
};
export default class Story extends React.Component {
constructor(...args) {
super(...args);
this.state = {
open: false,
stylesheet: this.props.styles(JSON.parse(JSON.stringify(stylesheet)))
};
MTRC.configure(this.props.mtrcConf);
}
componentWillReceiveProps(nextProps) {
this.setState({
stylesheet: nextProps.styles(JSON.parse(JSON.stringify(stylesheet)))
});
}
_renderStory() {
return (
<div>
{ this.props.children }
</div>
);
}
_renderInline() {
return (
<div>
<div style={this.state.stylesheet.infoPage}>
<div style={this.state.stylesheet.infoBody} >
{ this._getInfoHeader() }
</div>
</div>
<div>
{ this._renderStory() }
</div>
<div style={this.state.stylesheet.infoPage}>
<div style={this.state.stylesheet.infoBody} >
{ this._getInfoContent() }
{ this._getSourceCode() }
{ this._getPropTables() }
</div>
</div>
</div>
);
}
_renderOverlay() {
const linkStyle = {
...stylesheet.link.base,
...stylesheet.link.topRight,
};
const infoStyle = Object.assign({}, stylesheet.info);
if (!this.state.open) {
infoStyle.display = 'none';
}
const openOverlay = () => {
this.setState({ open: true });
return false;
};
const closeOverlay = () => {
this.setState({ open: false });
return false;
};
return (
<div>
<div style={this.state.stylesheet.children}>
{ this.props.children }
</div>
<a style={linkStyle} onClick={openOverlay}>?</a>
<div style={infoStyle}>
<a style={linkStyle} onClick={closeOverlay}>×</a>
<div style={this.state.stylesheet.infoPage}>
<div style={this.state.stylesheet.infoBody}>
{ this._getInfoHeader() }
{ this._getInfoContent() }
{ this._getSourceCode() }
{ this._getPropTables() }
</div>
</div>
</div>
</div>
);
}
_getInfoHeader() {
if (!this.props.context || !this.props.showHeader) {
return null;
}
return (
<div style={this.state.stylesheet.header.body}>
<h1 style={this.state.stylesheet.header.h1}>{this.props.context.kind}</h1>
<h2 style={this.state.stylesheet.header.h2}>{this.props.context.story}</h2>
</div>
);
}
_getInfoContent() {
if (!this.props.info) {
return '';
}
if (React.isValidElement(this.props.info)) {
return (
<div
style={this.props.showInline ?
stylesheet.jsxInfoContent : stylesheet.infoContent}
>
{this.props.info}
</div>
);
}
const lines = this.props.info.split('\n');
while (lines[0].trim() === '') {
lines.shift();
}
let padding = 0;
const matches = lines[0].match(/^ */);
if (matches) {
padding = matches[0].length;
}
const source = lines.map(s => s.slice(padding)).join('\n');
return (
<div style={this.state.stylesheet.infoContent}>
{MTRC(source).tree}
</div>
);
}
_getSourceCode() {
if (!this.props.showSource) {
return null;
}
return (
<div>
<h1 style={this.state.stylesheet.source.h1}>Story Source</h1>
<Pre>
{React.Children.map(this.props.children, (root, idx) => (
<Node key={idx} depth={0} node={root} />
))}
</Pre>
</div>
);
}
_getPropTables() {
const types = new Map();
if (this.props.propTables === null) {
return null;
}
if (!this.props.children) {
return null;
}
if (this.props.propTables) {
this.props.propTables.forEach(function (type) {
types.set(type, true);
});
}
// depth-first traverse and collect types
function extract(children) {
if (!children) {
return;
}
if (Array.isArray(children)) {
children.forEach(extract);
return;
}
if (children.props && children.props.children) {
extract(children.props.children);
}
if (typeof children === 'string' || typeof children.type === 'string') {
return;
}
if (children.type && !types.has(children.type)) {
types.set(children.type, true);
}
}
// extract components from children
extract(this.props.children);
const array = Array.from(types.keys());
array.sort(function (a, b) {
return (a.displayName || a.name) > (b.displayName || b.name);
});
const propTables = array.map((type, idx) => {
return (
<div key={idx}>
<h2 style={this.state.stylesheet.propTableHead}>"{type.displayName || type.name}" Component</h2>
<PropTable type={type} />
</div>
);
});
if (!propTables || propTables.length === 0) {
return null;
}
return (
<div>
<h1 style={this.state.stylesheet.source.h1}>Prop Types</h1>
{propTables}
</div>
);
return;
}
render() {
if (this.props.showInline) {
return this._renderInline();
}
return this._renderOverlay();
}
}
Story.displayName = 'Story';
Story.propTypes = {
context: React.PropTypes.object,
info: React.PropTypes.oneOfType([
React.PropTypes.string,
React.PropTypes.node,
]),
propTables: React.PropTypes.arrayOf(React.PropTypes.func),
showInline: React.PropTypes.bool,
showHeader: React.PropTypes.bool,
showSource: React.PropTypes.bool,
styles: React.PropTypes.func.isRequired,
children: React.PropTypes.oneOfType([
React.PropTypes.object,
React.PropTypes.array,
]),
mtrcConf: React.PropTypes.object
};
Story.defaultProps = {
showInline: false,
showHeader: true,
showSource: true,
mtrcConf: {}
};