diff --git a/addons/info/src/components/PropTable.js b/addons/info/src/components/PropTable.js index f353644784b8..301f7e341cbd 100644 --- a/addons/info/src/components/PropTable.js +++ b/addons/info/src/components/PropTable.js @@ -2,7 +2,10 @@ import PropTypes from 'prop-types'; import React from 'react'; + +import { Table, Td, Th } from '@storybook/components'; import PropVal from './PropVal'; +import PrettyPropType from './types/PrettyPropType'; const PropTypesMap = new Map(); @@ -13,41 +16,10 @@ Object.keys(PropTypes).forEach(typeName => { PropTypesMap.set(type.isRequired, typeName); }); -const stylesheet = { - propTable: { - marginLeft: -10, - borderSpacing: '10px 5px', - borderCollapse: 'separate', - }, -}; - const isNotEmpty = obj => obj && obj.props && Object.keys(obj.props).length > 0; -const renderDocgenPropType = propType => { - if (!propType) { - return 'unknown'; - } - - const name = propType.name; - - switch (name) { - case 'arrayOf': - return `${propType.value.name}[]`; - case 'instanceOf': - return propType.value; - case 'union': - return propType.raw; - case 'signature': - return propType.raw; - default: - return name; - } -}; - const hasDocgen = type => isNotEmpty(type.__docgenInfo); -const boolToString = value => (value ? 'yes' : 'no'); - const propsFromDocgen = type => { const props = {}; const docgenInfoProps = type.__docgenInfo.props; @@ -59,8 +31,8 @@ const propsFromDocgen = type => { props[property] = { property, - propType: renderDocgenPropType(propType), - required: boolToString(docgenInfoProp.required), + propType, + required: docgenInfoProp.required, description: docgenInfoProp.description, defaultValue: defaultValueDesc.value, }; @@ -75,21 +47,15 @@ const propsFromPropTypes = type => { if (type.propTypes) { Object.keys(type.propTypes).forEach(property => { const typeInfo = type.propTypes[property]; - const required = boolToString(typeInfo.isRequired === undefined); - const description = - type.__docgenInfo && type.__docgenInfo.props && type.__docgenInfo.props[property] - ? type.__docgenInfo.props[property].description - : null; + const required = typeInfo.isRequired === undefined; + const docgenInfo = + type.__docgenInfo && type.__docgenInfo.props && type.__docgenInfo.props[property]; + const description = docgenInfo ? docgenInfo.description : null; let propType = PropTypesMap.get(typeInfo) || 'other'; if (propType === 'other') { - if ( - type.__docgenInfo && - type.__docgenInfo.props && - type.__docgenInfo.props[property] && - type.__docgenInfo.props[property].type - ) { - propType = type.__docgenInfo.props[property].type.name; + if (docgenInfo && docgenInfo.type) { + propType = docgenInfo.type.name; } } @@ -137,40 +103,40 @@ export default function PropTable(props) { }; return ( - +
- - - - - + + + + + {array.map(row => - - - - + + + - + + )} -
propertypropTyperequireddefaultdescriptionpropertypropTyperequireddefaultdescription
+ {row.property} - - {row.propType} - - {row.required} - + + + + {row.required ? 'yes' : '-'} + {row.defaultValue === undefined ? '-' : } - + {row.description} -
+ ); } diff --git a/addons/info/src/components/types/ArrayOf.js b/addons/info/src/components/types/ArrayOf.js new file mode 100644 index 000000000000..5ec615798de1 --- /dev/null +++ b/addons/info/src/components/types/ArrayOf.js @@ -0,0 +1,19 @@ +import React from 'react'; + +import PrettyPropType from './PrettyPropType'; +import { TypeInfo } from './proptypes'; + +const ArrayOf = ({ propType }) => + + [ + + + + ] + ; + +ArrayOf.propTypes = { + propType: TypeInfo.isRequired, +}; + +export default ArrayOf; diff --git a/addons/info/src/components/types/Enum.js b/addons/info/src/components/types/Enum.js new file mode 100644 index 000000000000..de574d88598d --- /dev/null +++ b/addons/info/src/components/types/Enum.js @@ -0,0 +1,11 @@ +import React from 'react'; +import { TypeInfo } from './proptypes'; + +const Enum = ({ propType }) => + + {propType.value.map(({ value }) => value).join(' | ')} + ; + +Enum.propTypes = { + propType: TypeInfo.isRequired, +}; diff --git a/addons/info/src/components/types/InstanceOf.js b/addons/info/src/components/types/InstanceOf.js new file mode 100644 index 000000000000..27cc8ca8e318 --- /dev/null +++ b/addons/info/src/components/types/InstanceOf.js @@ -0,0 +1,13 @@ +import React from 'react'; +import { TypeInfo } from './proptypes'; + +const InstanceOf = ({ propType }) => + + {propType.value} + ; + +InstanceOf.propTypes = { + propType: TypeInfo.isRequired, +}; + +export default InstanceOf; diff --git a/addons/info/src/components/types/Object.js b/addons/info/src/components/types/Object.js new file mode 100644 index 000000000000..b9fa4cfd0662 --- /dev/null +++ b/addons/info/src/components/types/Object.js @@ -0,0 +1,8 @@ +import React from 'react'; + +const ObjectType = () => + + {'{}'} + ; + +export default ObjectType; diff --git a/addons/info/src/components/types/ObjectOf.js b/addons/info/src/components/types/ObjectOf.js new file mode 100644 index 000000000000..c4d7d61a146e --- /dev/null +++ b/addons/info/src/components/types/ObjectOf.js @@ -0,0 +1,17 @@ +import React from 'react'; + +import PrettyPropType from './PrettyPropType'; +import { TypeInfo } from './proptypes'; + +const ObjectOf = ({ propType }) => + + {'{[]: '} + + {'}'} + ; + +ObjectOf.propTypes = { + propType: TypeInfo.isRequired, +}; + +export default ObjectOf; diff --git a/addons/info/src/components/types/ObjectType.js b/addons/info/src/components/types/ObjectType.js new file mode 100644 index 000000000000..b9fa4cfd0662 --- /dev/null +++ b/addons/info/src/components/types/ObjectType.js @@ -0,0 +1,8 @@ +import React from 'react'; + +const ObjectType = () => + + {'{}'} + ; + +export default ObjectType; diff --git a/addons/info/src/components/types/OneOf.js b/addons/info/src/components/types/OneOf.js new file mode 100644 index 000000000000..65177f77e165 --- /dev/null +++ b/addons/info/src/components/types/OneOf.js @@ -0,0 +1,13 @@ +import React from 'react'; +import { TypeInfo } from './proptypes'; + +const OneOf = ({ propType }) => + + {propType.value.map(({ value }) => value).join(' | ')} + ; + +OneOf.propTypes = { + propType: TypeInfo.isRequired, +}; + +export default OneOf; diff --git a/addons/info/src/components/types/OneOfType.js b/addons/info/src/components/types/OneOfType.js new file mode 100644 index 000000000000..901cd5621065 --- /dev/null +++ b/addons/info/src/components/types/OneOfType.js @@ -0,0 +1,25 @@ +import React from 'react'; + +import PrettyPropType from './PrettyPropType'; +import { TypeInfo } from './proptypes'; + +const OneOfType = ({ propType }) => { + const length = propType.value.length; + return ( + + {propType.value + .map((value, i) => [ + , + i < length - 1 ? | : null, + ]) + .reduce((acc, tuple) => acc.concat(tuple), [])} + + ); +}; +OneOfType.propTypes = { + propType: TypeInfo.isRequired, +}; +export default OneOfType; diff --git a/addons/info/src/components/types/PrettyPropType.js b/addons/info/src/components/types/PrettyPropType.js new file mode 100644 index 000000000000..fe50881408f7 --- /dev/null +++ b/addons/info/src/components/types/PrettyPropType.js @@ -0,0 +1,61 @@ +import PropTypes from 'prop-types'; +import React from 'react'; + +import ObjectType from './ObjectType'; +import Shape from './Shape'; +import OneOfType from './OneOfType'; +import ArrayOf from './ArrayOf'; +import ObjectOf from './ObjectOf'; +import OneOf from './OneOf'; +import InstanceOf from './InstanceOf'; +import Signature from './Signature'; + +import { TypeInfo } from './proptypes'; + +// propType -> Component map - these are a bit more complex prop types to display +const propTypeComponentMap = new Map([ + ['shape', Shape], + ['union', OneOfType], + ['arrayOf', ArrayOf], + ['objectOf', ObjectOf], + // Might be overkill to have below proptypes as separate components *shrug* + ['object', ObjectType], + ['enum', OneOf], + ['instanceOf', InstanceOf], + ['signature', Signature], +]); + +const PrettyPropType = props => { + const { propType, depth } = props; + if (!propType) { + return unknown; + } + + const { name } = propType || {}; + + if (propTypeComponentMap.has(name)) { + const Component = propTypeComponentMap.get(name); + return ; + } + + // Otherwise, propType does not have a dedicated component, display proptype name by default + return ( + + {name} + + ); +}; + +PrettyPropType.displayName = 'PrettyPropType'; + +PrettyPropType.defaultProps = { + propType: null, + depth: 1, +}; + +PrettyPropType.propTypes = { + propType: TypeInfo, + depth: PropTypes.number.isRequired, +}; + +export default PrettyPropType; diff --git a/addons/info/src/components/types/PropertyLabel.js b/addons/info/src/components/types/PropertyLabel.js new file mode 100644 index 000000000000..8eca3d423256 --- /dev/null +++ b/addons/info/src/components/types/PropertyLabel.js @@ -0,0 +1,31 @@ +import PropTypes from 'prop-types'; +import React from 'react'; + +const styles = { + hasProperty: { + whiteSpace: 'nowrap', + }, +}; + +const PropertyLabel = ({ property, required }) => { + if (!property) return null; + + return ( + + {property} + {required ? '' : '?'}:{' '} + + ); +}; + +PropertyLabel.propTypes = { + property: PropTypes.string, + required: PropTypes.bool, +}; + +PropertyLabel.defaultProps = { + property: '', + required: false, +}; + +export default PropertyLabel; diff --git a/addons/info/src/components/types/Shape.js b/addons/info/src/components/types/Shape.js new file mode 100644 index 000000000000..7a2f114ef178 --- /dev/null +++ b/addons/info/src/components/types/Shape.js @@ -0,0 +1,81 @@ +import PropTypes from 'prop-types'; +import React from 'react'; + +import { HighlightButton } from '@storybook/components'; +import PrettyPropType from './PrettyPropType'; +import PropertyLabel from './PropertyLabel'; + +import { TypeInfo } from './proptypes'; + +const MARGIN_SIZE = 15; + +class Shape extends React.Component { + constructor(props) { + super(props); + this.state = { + minimized: false, + }; + } + + handleToggle = () => { + this.setState({ + minimized: !this.state.minimized, + }); + }; + + handleMouseEnter = () => { + this.setState({ hover: true }); + }; + + handleMouseLeave = () => { + this.setState({ hover: false }); + }; + + render() { + const { propType, depth } = this.props; + return ( + + + {'{'} + + ... + {!this.state.minimized && + Object.keys(propType.value).map(childProperty => +
+ + + , +
+ )} + + + {'}'} + +
+ ); + } +} + +Shape.propTypes = { + propType: TypeInfo, + depth: PropTypes.number.isRequired, +}; + +Shape.defaultProps = { + propType: null, +}; + +export default Shape; diff --git a/addons/info/src/components/types/Signature.js b/addons/info/src/components/types/Signature.js new file mode 100644 index 000000000000..5f85770b45e4 --- /dev/null +++ b/addons/info/src/components/types/Signature.js @@ -0,0 +1,13 @@ +import React from 'react'; +import { TypeInfo } from './proptypes'; + +const Signature = ({ propType }) => + + {propType.raw} + ; + +Signature.propTypes = { + propType: TypeInfo.isRequired, +}; + +export default Signature; diff --git a/addons/info/src/components/types/proptypes.js b/addons/info/src/components/types/proptypes.js new file mode 100644 index 000000000000..916bc3404acb --- /dev/null +++ b/addons/info/src/components/types/proptypes.js @@ -0,0 +1,6 @@ +import PropTypes from 'prop-types'; + +export const TypeInfo = PropTypes.shape({ + name: PropTypes.string, + value: PropTypes.any, +}); diff --git a/examples/cra-kitchen-sink/src/components/DocgenButton.js b/examples/cra-kitchen-sink/src/components/DocgenButton.js index d6835f1ff470..20e997d58eb1 100644 --- a/examples/cra-kitchen-sink/src/components/DocgenButton.js +++ b/examples/cra-kitchen-sink/src/components/DocgenButton.js @@ -54,6 +54,46 @@ DocgenButton.propTypes = { ), }) ), + + /** + * Plain object propType (use shape!!) + */ + obj: PropTypes.object, // eslint-disable-line react/forbid-prop-types + + /** + * propType for shape with nested arraytOf + */ + 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, + }), + }), + }), + + arrayOf: PropTypes.arrayOf(PropTypes.number), + /** * `instanceOf` is also supported and the custom type will be shown instead of `instanceOf` */ diff --git a/examples/cra-kitchen-sink/src/stories/Container.js b/examples/cra-kitchen-sink/src/stories/Container.js index aebe66d28126..efd894da18a1 100644 --- a/examples/cra-kitchen-sink/src/stories/Container.js +++ b/examples/cra-kitchen-sink/src/stories/Container.js @@ -1,26 +1,30 @@ import React from 'react'; import PropTypes from 'prop-types'; -const Container = ({ children, title, age, isAmazing }) => +const Container = ({ children, title, age, isAmazing }) => (
{children} {isAmazing ? '!!!' : ''} - {age - ?
- age = {age} -
- : null} -
; + {age.isOld ?
age = {age.value}
: null} + +); Container.propTypes = { + /** The nodes to be rendered in the button */ children: PropTypes.node.isRequired, + /** Show exclamation marks */ isAmazing: PropTypes.bool, - age: PropTypes.number, + /** Show age prop */ + age: PropTypes.shape({ + isOld: PropTypes.bool, + value: PropTypes.number, + }), + /** Main title */ title: PropTypes.string, }; Container.defaultProps = { isAmazing: false, - age: 0, + age: { isOld: false, value: 0 }, title: 'the best container ever', }; diff --git a/lib/components/src/highlight_button.js b/lib/components/src/highlight_button.js new file mode 100644 index 000000000000..609bdc5aa419 --- /dev/null +++ b/lib/components/src/highlight_button.js @@ -0,0 +1,27 @@ +import glamorous from 'glamorous'; + +export default glamorous.button( + { + border: '1px solid rgba(0, 0, 0, 0)', + font: 'inherit', + background: 'none', + 'box-shadow': 'none', + padding: 0, + ':hover': { + backgroundColor: 'rgba(0, 0, 0, 0.05)', + border: '1px solid #ccc', + }, + }, + props => { + const styles = []; + + if (props.highlight) { + styles.push({ + backgroundColor: 'rgba(0, 0, 0, 0.05)', + border: '1px solid #ccc', + }); + } + + return styles; + } +); diff --git a/lib/components/src/index.js b/lib/components/src/index.js index 446888e9dc92..7e73841424ad 100644 --- a/lib/components/src/index.js +++ b/lib/components/src/index.js @@ -2,3 +2,6 @@ export { baseFonts } from './theme'; export { default as RoutedLink } from './navigation/routed_link'; export { default as MenuLink } from './navigation/menu_link'; +export { default as HighlightButton } from './highlight_button'; +export { default as Table } from './table/table'; +export { td as Td, th as Th } from './table/cell'; diff --git a/lib/components/src/table/cell.js b/lib/components/src/table/cell.js new file mode 100644 index 000000000000..fa23e60876c6 --- /dev/null +++ b/lib/components/src/table/cell.js @@ -0,0 +1,27 @@ +import glamorous from 'glamorous'; + +const dynamicStyles = props => { + const styles = []; + + if (props.bordered) { + styles.push({ + border: '1px solid #ccc', + }); + } + + if (props.code) { + styles.push({ + whiteSpace: 'nowrap', + fontFamily: 'Monaco, Consolas, "Courier New", monospace', + }); + } + + return styles; +}; + +const styles = { + padding: '2px 6px', +}; + +export const td = glamorous.td(styles, dynamicStyles); +export const th = glamorous.th(styles, dynamicStyles); diff --git a/lib/components/src/table/table.js b/lib/components/src/table/table.js new file mode 100644 index 000000000000..2871c6ca9154 --- /dev/null +++ b/lib/components/src/table/table.js @@ -0,0 +1,5 @@ +import glamorous from 'glamorous'; + +export default glamorous.table({ + borderCollapse: 'collapse', +});