UNPKG

react-storybook-addon-chapters

Version:

React Storybook Chapters addon allows showcasing of multiple components within a story by breaking it down into smaller categories (chapters) and subcategories (sections) for more organizational goodness.

357 lines (320 loc) 8.56 kB
import React, { Component } from 'react'; import PropTypes from 'prop-types'; import Node from './Node'; import PropTable from './PropTable'; import renderInfoContent from '../utils/info-content'; import theme from '../theme'; const propTypes = { title: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), subtitle: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), info: PropTypes.string, showSource: PropTypes.bool, showPropTables: PropTypes.bool, propTables: PropTypes.arrayOf(PropTypes.func), children: PropTypes.oneOfType([PropTypes.object, PropTypes.array]).isRequired, allowSourceToggling: PropTypes.bool, allowPropTablesToggling: PropTypes.bool, addonInfo: PropTypes.object.isRequired, useTheme: PropTypes.bool }; const defaultProps = { title: '', subtitle: '', info: '', showSource: true, allowSourceToggling: true, showPropTables: false, allowPropTablesToggling: true, useTheme: false }; export const sectionButtonStyles = { backgroundColor: 'transparent', border: `1px solid ${theme.gray}`, borderRadius: 3, color: theme.grayDark, cursor: 'pointer', float: 'right', marginLeft: 5, padding: '5px 10px' }; export const sectionStyles = { container: { marginBottom: 100 }, header: { marginBottom: 60 }, title: { color: theme.grayDarkest, fontSize: 18, marginBottom: 10 }, subtitle: { color: theme.grayDark, fontSize: 14, marginBottom: 20, marginTop: 0 }, buttonContainer: { height: 15 }, button: sectionButtonStyles, 'button-active': { ...sectionButtonStyles, backgroundColor: theme.grayLight, borderColor: theme.grayLight, color: theme.grayDark }, info: theme.infoStyle, componentContainer: { marginBottom: 60 }, subsection: { marginBottom: 60 }, subsectionTitle: { color: theme.grayDark, fontSize: 12, letterSpacing: 2, textTransform: 'uppercase' }, pre: { fontSize: '.88em', fontFamily: 'Menlo, Monaco, "Courier New", monospace', backgroundColor: 'rgb(250, 250, 250)', padding: '0.5rem', lineHeight: '1.5', overflowX: 'scroll' } }; export class SectionDecorator { static main(header, component, additional, useTheme) { return ( <div style={useTheme ? sectionStyles.container : {}} className="section-container"> {header} {component} {additional} </div> ); } static header(header, useTheme) { return ( <div style={useTheme ? sectionStyles.header : {}} className="section-header"> {header} </div> ); } static title(title, useTheme) { return ( <h3 style={useTheme ? sectionStyles.title : {}} className="section-title"> {title} </h3> ); } static subtitle(subtitle, useTheme) { return ( <p style={useTheme ? sectionStyles.subtitle : {}} className="section-subtitle"> {subtitle} </p> ); } static component(component, useTheme, decorator) { const decoratedComponent = decorator ? decorator(() => component) : component; return ( <div style={useTheme ? sectionStyles.componentContainer : {}} className="section-component-container"> {decoratedComponent} </div> ); } static additional(additional, useTheme) { return ( <div style={useTheme ? sectionStyles.additional : {}} className="section-additional"> {additional} </div> ); } static sourceCode(sourceCode, useTheme) { return ( <div style={useTheme ? sectionStyles.subsection : {}} className="section-subsection"> <h4 style={useTheme ? sectionStyles.subsection.title : {}} className="section-subsection-title"> Source </h4> <pre style={useTheme ? sectionStyles.pre : {}} className="section-pre"> {sourceCode} </pre> </div> ); } static propTables(propTables, useTheme) { return ( <div style={useTheme ? sectionStyles.subsection : {}} className="section-subsection"> <h4 style={useTheme ? sectionStyles.subsection.title : {}} className="section-subsection-title"> PropTypes </h4> {propTables} </div> ); } static buttons(buttons, useTheme) { return ( <div style={useTheme ? sectionStyles.buttonContainer : {}} className="section-button-container"> {buttons} </div> ); } static info(infoContent, useTheme) { return ( <div style={useTheme ? sectionStyles.subsection : {}} className="section-subsection"> <div style={useTheme ? sectionStyles.info : {}} className="section-info"> {infoContent} </div> </div> ); } } export default class Section extends Component { constructor(props) { super(props); this.state = { isSourceShown: props.showSource, isPropsTableShown: props.showPropTables }; } renderSourceCode(useTheme) { const { addonInfo, children } = this.props; const sourceCode = React.Children.map(children, (root, idx) => ( <Node key={idx} depth={0} node={root} {...addonInfo} {...this.props} /> )); return SectionDecorator.sourceCode(sourceCode, useTheme); } renderPropTables(useTheme) { const { children, propTables } = this.props; const components = new Map(); if (!children) { return null; } if (propTables) { propTables.forEach(function(component) { components.set(component, true); }); } // Depth-first traverse and collect components. function extract(traverseChildren) { if (!traverseChildren) { return; } if (Array.isArray(traverseChildren)) { traverseChildren.forEach(extract); return; } if (traverseChildren.props && traverseChildren.props.children) { extract(traverseChildren.props.children); } if (typeof traverseChildren === 'string' || typeof traverseChildren.type === 'string') { return; } if (traverseChildren.type && !components.has(traverseChildren.type)) { components.set(traverseChildren.type, true); } } // Extract components from children. extract(children); const componentsList = Array.from(components.keys()); componentsList.sort(function(a, b) { return (a.displayName || a.name) > (b.displayName || b.name); }); const newPropTables = componentsList.map(function(component, i) { return ( <div key={i}> <h5> &lt; {component.displayName || component.name} &gt; Component </h5> <PropTable component={component} useTheme={useTheme} /> </div> ); }); if (!newPropTables || newPropTables.length === 0) { return null; } return SectionDecorator.propTables(newPropTables, useTheme); } render() { const { title, subtitle, children, info, allowPropTablesToggling, allowSourceToggling, useTheme, decorator } = this.props; const showButtonsRow = allowPropTablesToggling || allowSourceToggling; const { isPropsTableShown, isSourceShown } = this.state; const header = ( <div> {title && SectionDecorator.title(title, useTheme)} {subtitle && SectionDecorator.subtitle(subtitle, useTheme)} </div> ); const buttons = [ allowPropTablesToggling && ( <button type="button" key="allowPropTablesToggling" onClick={() => { this.setState({ isPropsTableShown: !this.state.isPropsTableShown }); }} style={ useTheme ? isPropsTableShown ? sectionStyles['button-active'] : sectionStyles.button : isPropsTableShown ? sectionStyles['button-active'] : sectionStyles.button } className={isPropsTableShown ? 'button-active' : 'button'} > {isPropsTableShown ? 'Hide' : 'Show'} Props Table </button> ), allowSourceToggling && ( <button type="button" key="allowSourceToggling" onClick={() => { this.setState({ isSourceShown: !isSourceShown }); }} style={ useTheme ? isSourceShown ? sectionStyles['button-active'] : sectionStyles.button : isSourceShown ? sectionStyles['button-active'] : sectionStyles.button } className={isSourceShown ? 'button-active' : 'button'} > {isSourceShown ? 'Hide' : 'Show'} Source </button> ) ]; const additional = ( <div> {info && SectionDecorator.info(renderInfoContent(info), useTheme)} {showButtonsRow && SectionDecorator.buttons(buttons, useTheme)} {isSourceShown && this.renderSourceCode(useTheme)} {isPropsTableShown && this.renderPropTables(useTheme)} </div> ); return SectionDecorator.main( SectionDecorator.header(header), SectionDecorator.component(children, useTheme, decorator), SectionDecorator.additional(additional), useTheme ); } } Section.displayName = 'Section'; Section.propTypes = propTypes; Section.defaultProps = defaultProps;