From 769467f761d8d61e2e446a24cf20613805104ee0 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Wed, 15 Apr 2020 13:32:41 -0700 Subject: [PATCH 1/5] Add first iteration of wrapped PropTypes --- src/PropsDocs/addDocumentedProps.js | 24 ++++++ src/PropsDocs/index.js | 2 + src/PropsDocs/propTypes.js | 123 ++++++++++++++++++++++++++++ 3 files changed, 149 insertions(+) create mode 100644 src/PropsDocs/addDocumentedProps.js create mode 100644 src/PropsDocs/index.js create mode 100644 src/PropsDocs/propTypes.js diff --git a/src/PropsDocs/addDocumentedProps.js b/src/PropsDocs/addDocumentedProps.js new file mode 100644 index 00000000000..0822cd2e87b --- /dev/null +++ b/src/PropsDocs/addDocumentedProps.js @@ -0,0 +1,24 @@ +function getPropTypesFromArray(ary, propTypes) { + return ary.reduce((acc, item) => { + Object.assign(acc, item[propTypes]) + return acc + }, {}) +} + +export default function addDocumentedProps(Component, props) { + const system = props.system || [] + const inherited = props.inherited || [] + const own = props.own || {} + + Component.propTypes = { + ...getPropTypesFromArray(system, 'propTypes'), + ...getPropTypesFromArray(inherited, 'propTypes'), + ...own + } + + Component.propTypesDocumented = { + system, + inherited, + own + } +} diff --git a/src/PropsDocs/index.js b/src/PropsDocs/index.js new file mode 100644 index 00000000000..99f4524fbff --- /dev/null +++ b/src/PropsDocs/index.js @@ -0,0 +1,2 @@ +export {default as addDocumentedProps} from './addDocumentedProps' +export {propTypes as PropTypes, wrapPrimitivePropType, wrapCallablePropType} from './propTypes' diff --git a/src/PropsDocs/propTypes.js b/src/PropsDocs/propTypes.js new file mode 100644 index 00000000000..855a5b3dfff --- /dev/null +++ b/src/PropsDocs/propTypes.js @@ -0,0 +1,123 @@ +import PropTypes from 'prop-types' + +function proxyPropTypes(target, wrapper, names) { + for (const name of names) { + Object.defineProperty(target, name, { + configurable: false, + enumerable: true, + get() { + return wrapper(PropTypes[name], name) + } + }) + } +} + +function addDocKeys(checker, isRequired, name, args = []) { + function newIsRequired(...args) { + return isRequired(...args) + } + + Object.defineProperties(checker, { + doc: { + configurable: false, + enumerable: false, + value: { + name, + hidden: false, + isRequired: false, + desc: '', + args + } + }, + desc: { + configurable: false, + enumerable: false, + value: desc => { + checker.doc.desc = desc + return checker + } + }, + hidden: { + configurable: false, + enumerable: false, + get() { + checker.doc.hidden = true + return checker + } + }, + isRequired: { + configurable: false, + enumerable: false, + value: newIsRequired + } + }) + + Object.defineProperties(newIsRequired, { + doc: { + configurable: false, + enumerable: false, + get() { + return checker.doc + } + }, + desc: { + configurable: false, + enumerable: false, + value: desc => { + checker.doc.desc = desc + return newIsRequired + } + }, + hidden: { + configurable: false, + enumerable: false, + get() { + checker.doc.hidden = true + return newIsRequired + } + } + }) +} + +export function wrapPrimitivePropType(propType, name) { + const checker = (...args) => propType(...args) + const origIsRequired = propType.isRequired + addDocKeys(checker, origIsRequired, name) + return checker +} + +export function wrapCallablePropType(propType, name) { + return function checkerCreator(args) { + const checker = propType(args) + const origIsRequired = checker.isRequired + addDocKeys(checker, origIsRequired, name, args) + return checker + } +} + +const propTypes = {} + +proxyPropTypes(propTypes, wrapPrimitivePropType, [ + 'any', + 'array', + 'bool', + 'func', + 'number', + 'object', + 'string', + 'symbol', + 'node', + 'element', + 'elementType' +]) +proxyPropTypes(propTypes, wrapCallablePropType, [ + 'instanceOf', + 'arrayOf', + 'oneOf', + 'oneOfType', + 'objectOf', + 'shape', + 'exact' +]) + +export {propTypes} From f2b6652048ff08fe2457780a37960c1a643abe00 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Wed, 15 Apr 2020 13:33:07 -0700 Subject: [PATCH 2/5] Add ugly, hacky prop display component --- docs/components/ComponentProps.js | 294 ++++++++++++++++++++++++++++++ 1 file changed, 294 insertions(+) create mode 100644 docs/components/ComponentProps.js diff --git a/docs/components/ComponentProps.js b/docs/components/ComponentProps.js new file mode 100644 index 00000000000..f4dcbf0e3c2 --- /dev/null +++ b/docs/components/ComponentProps.js @@ -0,0 +1,294 @@ +import React from 'react' +import styled from 'styled-components' +import Link from '../../src/Link' +import {H1, H2, H3, H4, H5, H6} from '@primer/gatsby-theme-doctocat/src/components/heading' +import BorderBox from '../../src/BorderBox' +import Button from '../../src/Button' +import Text from '../../src/Text' +import Details from '../../src/Details' +import InlineCode from '@primer/gatsby-theme-doctocat/src/components/inline-code' +import Paragraph from '@primer/gatsby-theme-doctocat/src/components/paragraph' +import Table from '@primer/gatsby-theme-doctocat/src/components/table' + +function getHeadingElement(headingLevel) { + switch (headingLevel) { + case 1: + return H1 + case 2: + return H2 + case 3: + return H3 + case 4: + return H4 + case 5: + return H5 + case 6: + return H6 + } +} + +const InheritedBox = styled(BorderBox)` + > :first-child { + margin-top: 0px !important; + } +` + +function collect(inherited, acc = {system: [], inherited: []}, seen = new Set()) { + for (const Comp of inherited) { + if (Comp.propTypesDocumented) { + const {system, inherited: nestedInherited} = Comp.propTypesDocumented + for (const sys of system) { + if (!seen.has(sys)) { + acc.system.push(sys) + seen.add(sys) + } + } + for (const inh of nestedInherited) { + if (!seen.has(inh)) { + acc.inherited.push(inh) + seen.add(inh) + } + } + if (nestedInherited.length) { + collect(nestedInherited, acc, seen) + } + } + } + + return acc +} + +function ComponentProps({Component, name, headingLevel, showInherited, showSystem}) { + if (!Component.propTypesDocumented) { + return null + } + const Heading = getHeadingElement(headingLevel) + + const {own} = Component.propTypesDocumented + const {system, inherited} = collect([Component]) + + const output = [] + + if (own) { + output.push( + + ) + } + + const inheritedWithDocs = inherited.filter(Comp => Comp.propTypesDocumented) + if (inheritedWithDocs.length && showInherited) { + output.push() + output.push( + + Inherited props + + {name} inherits from the following components and thus receives their props: + + + ) + for (const Comp of inherited) { + output.push( +
+ {({open}) => ( + <> + + + + + + )} +
+ ) + } + } + + if (showSystem && system && system.length) { + output.push() + } + + return <>{output} +} + +ComponentProps.defaultProps = { + headingLevel: 3, + showInherited: true, + showSystem: true +} + +function getDefault(defaults, prop) { + const value = defaults[prop] + return getDisplayValue(value) +} + +function getDisplayValue(value, key) { + const type = typeof value + + if (type === 'object') { + return '(object)' + } + + if (type === 'string') { + return "{value}" + } + + if (type === 'number' || type === 'boolean') { + return {String(value)} + } + + if (type === 'function') { + return (function) + } + + return value +} + +const PropValueList = styled.ul` + margin-block-start: 0; + margin-block-end: 0; + margin-left: -20px; +` + +function getType(doc) { + switch (doc.name) { + case 'any': + return 'any' + case 'array': + return 'array' + case 'bool': + return 'boolean' + case 'func': + return 'function' + case 'number': + return 'number' + case 'node': + return 'node' + case 'object': + return 'object' + case 'string': + return 'string' + case 'symbol': + return 'symbol' + case 'element': + return 'element' + case 'elementType': + return 'element type' + + case 'instanceOf': + return `instance of ${doc.args.name}` + case 'arrayOf': + return `array of ${getType(doc.args)}s` + case 'oneOf': { + const items = doc.args.map((item, idx) => getDisplayValue(item, idx)) + return ( + <> + One of: + + {items.map((item, idx) => ( + // eslint-disable-next-line react/no-array-index-key +
  • {item}
  • + ))} +
    + + ) + } + case 'oneOfType': { + const items = doc.args.map(item => getType(item.doc)) + return ( + <> + One of type: + + {items.map((item, idx) => ( + // eslint-disable-next-line react/no-array-index-key +
  • {item}
  • + ))} +
    + + ) + } + default: + return '(unknown type)' + } +} + +function OwnProps({props, defaults, headingLevel}) { + const Heading = getHeadingElement(headingLevel) + const propsToShow = Object.keys(props).filter(key => !props[key].doc.hidden) + if (propsToShow.length === 0) { + return ( + <> + Component props + This component gets no additional props. + + ) + } + return ( + <> + Component props + + + + + + + + + + + {propsToShow.map(prop => { + return ( + + + + + + + ) + })} + +
    NameTypeDefault valueDescription
    {prop}{getType(props[prop].doc)}{getDefault(defaults, prop)}{props[prop].doc.desc}
    + + ) +} + +function SystemProps({name, systemProps, headingLevel}) { + const Heading = getHeadingElement(headingLevel) + return ( + <> + System props + + {name} components receive the following categories of system props. See our{' '} + System Props page for more information. + + + + + + + + + + {systemProps.map(s => ( + + + + + ))} + +
    CategoryIncluded props
    + {s.systemPropsName} + +
    {Object.keys(s.propTypes).join(', ')}
    +
    + + ) +} + +export default ComponentProps From 130349cdd272e2c9544393962920344c07a2403b Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Wed, 15 Apr 2020 13:33:31 -0700 Subject: [PATCH 3/5] Use wrapped PropTypes on some components --- src/BorderBox.js | 20 ++++++++++++----- src/Box.js | 21 ++++++++---------- src/Link.js | 25 +++++++++++---------- src/SideNav.js | 47 +++++++++++++++++++++++++--------------- src/constants.js | 10 ++++++++- src/utils/elementType.js | 5 +++-- 6 files changed, 79 insertions(+), 49 deletions(-) diff --git a/src/BorderBox.js b/src/BorderBox.js index 2cdde7b595d..0072c70f11d 100644 --- a/src/BorderBox.js +++ b/src/BorderBox.js @@ -1,5 +1,5 @@ import styled from 'styled-components' -import PropTypes from 'prop-types' +import {addDocumentedProps, PropTypes} from './PropsDocs' import Box from './Box' import theme from './theme' import {BORDER} from './constants' @@ -13,10 +13,18 @@ BorderBox.defaultProps = { borderRadius: 2 } -BorderBox.propTypes = { - theme: PropTypes.object, - ...Box.propTypes, - ...BORDER.propTypes -} +addDocumentedProps(BorderBox, { + system: [BORDER], + inherited: [Box], + own: { + border: PropTypes.string.desc('Sets the border; use theme values or provide your own'), + borderColor: PropTypes.string.desc('Sets the border; use theme values or provide your own'), + borderRadius: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).desc( + 'Sets the border radius, use theme values or provide your own' + ), + boxShadow: PropTypes.string.desc('Sets box shadow, use theme values or provide your own'), + theme: PropTypes.object.hidden + } +}) export default BorderBox diff --git a/src/Box.js b/src/Box.js index 82b53baf5eb..d9298661ffd 100644 --- a/src/Box.js +++ b/src/Box.js @@ -1,23 +1,20 @@ import styled from 'styled-components' -import PropTypes from 'prop-types' -import {space, color} from 'styled-system' -import systemPropTypes from '@styled-system/prop-types' -import {LAYOUT} from './constants' +import {addDocumentedProps, PropTypes} from './PropsDocs' +import {COMMON, LAYOUT} from './constants' import theme from './theme' const Box = styled.div` ${LAYOUT} - ${space} - ${color} + ${COMMON} ` Box.defaultProps = {theme} -Box.propTypes = { - ...LAYOUT.propTypes, - ...systemPropTypes.space, - ...systemPropTypes.color, - theme: PropTypes.object -} +addDocumentedProps(Box, { + system: [COMMON, LAYOUT], + own: { + theme: PropTypes.object.hidden + } +}) export default Box diff --git a/src/Link.js b/src/Link.js index e0915715f73..04228369bb9 100644 --- a/src/Link.js +++ b/src/Link.js @@ -1,4 +1,4 @@ -import PropTypes from 'prop-types' +import {addDocumentedProps, PropTypes} from './PropsDocs' import styled from 'styled-components' import {system} from 'styled-system' import {COMMON, TYPOGRAPHY, get} from './constants' @@ -37,17 +37,20 @@ const Link = styled.a.attrs(props => ({ ` Link.defaultProps = { - theme + muted: false, + theme, + underline: false } -Link.propTypes = { - as: elementType, - href: PropTypes.string, - muted: PropTypes.bool, - theme: PropTypes.object, - underline: PropTypes.bool, - ...TYPOGRAPHY.propTypes, - ...COMMON.propTypes -} +addDocumentedProps(Link, { + system: [COMMON, TYPOGRAPHY], + own: { + as: elementType.desc("Can be 'a', 'button', 'input', or 'summary'"), + href: PropTypes.string.desc('URL to be used for the Link'), + muted: PropTypes.bool.desc('Uses light gray for Link color, and blue on hover'), + theme: PropTypes.object.hidden, + underline: PropTypes.bool.desc('Adds underline to the Link') + } +}) export default Link diff --git a/src/SideNav.js b/src/SideNav.js index 517954d063f..78666b176f9 100644 --- a/src/SideNav.js +++ b/src/SideNav.js @@ -1,7 +1,7 @@ import React from 'react' -import PropTypes from 'prop-types' import styled, {css} from 'styled-components' import classnames from 'classnames' +import {addDocumentedProps, PropTypes} from './PropsDocs' import {COMMON, get} from './constants' import theme from './theme' import elementType from './utils/elementType' @@ -140,30 +140,43 @@ SideNav.Link = styled(Link).attrs(props => { SideNav.defaultProps = { theme, + bordered: false, variant: 'normal' } -SideNav.propTypes = { - as: elementType, - bordered: PropTypes.bool, - children: PropTypes.node, - theme: PropTypes.object, - variant: PropTypes.oneOf(['normal', 'lightweight']), - ...BorderBox.propTypes, - ...COMMON.propTypes -} +addDocumentedProps(SideNav, { + system: [COMMON], + inherited: [BorderBox], + own: { + as: elementType.desc('Sets the HTML tag for the element'), + bordered: PropTypes.bool.desc('Renders the component with a border'), + children: PropTypes.node.hidden, + theme: PropTypes.object.hidden, + variant: PropTypes.oneOf(['normal', 'lightweight']).desc( + 'Set to `lightweight` to render [in a lightweight style](#lightweight-variant)' + ) + } +}) SideNav.Link.defaultProps = { theme, + selected: false, variant: 'normal' } -SideNav.Link.propTypes = { - as: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), - selected: PropTypes.bool, - theme: PropTypes.object, - variant: PropTypes.oneOf(['normal', 'full']), - ...Link.propTypes -} +addDocumentedProps(SideNav.Link, { + system: [], + inherited: [Link], + own: { + as: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).desc('Sets the HTML tag for the element'), + selected: PropTypes.bool.desc( + 'Sets the link as selected, giving it a different style and setting the `aria-current` attribute' + ), + theme: PropTypes.object.hidden, + variant: PropTypes.oneOf(['normal', 'full']).desc( + 'Set to `full` to render a [full variant](#full-variant), suitable for including icons and labels' + ) + } +}) export default SideNav diff --git a/src/constants.js b/src/constants.js index 9f48f7745b2..9106b23829f 100644 --- a/src/constants.js +++ b/src/constants.js @@ -25,13 +25,16 @@ TYPOGRAPHY.propTypes = { export const COMMON = compose(styledSystem.space, styledSystem.color, styledSystem.display) COMMON.propTypes = { ...systemPropTypes.space, - ...systemPropTypes.color + ...systemPropTypes.color, + ...systemPropTypes.display } +COMMON.systemPropsName = 'COMMON' export const BORDER = compose(styledSystem.border, styledSystem.shadow) BORDER.propTypes = { ...systemPropTypes.border, ...systemPropTypes.shadow } +BORDER.systemPropsName = 'BORDER' // these are 1:1 with styled-system's API now, // so you could consider dropping the abstraction @@ -41,7 +44,12 @@ export const FLEX = styledSystem.flexbox export const GRID = styledSystem.grid TYPOGRAPHY.propTypes = systemPropTypes.typography +TYPOGRAPHY.systemPropsName = 'TYPOGRAPHY' LAYOUT.propTypes = systemPropTypes.layout +LAYOUT.systemPropsName = 'LAYOUT' POSITION.propTypes = systemPropTypes.position +POSITION.systemPropsName = 'POSITION' FLEX.propTypes = systemPropTypes.flexbox +FLEX.systemPropsName = 'FLEX' GRID.propTypes = systemPropTypes.grid +GRID.systemPropsName = 'GRID' diff --git a/src/utils/elementType.js b/src/utils/elementType.js index a4e72c3581a..f8d8eb80414 100644 --- a/src/utils/elementType.js +++ b/src/utils/elementType.js @@ -1,14 +1,15 @@ +import {wrapPrimitivePropType} from '../PropsDocs' import {isValidElementType} from 'react-is' // This function is a temporary workaround until we can get // the official PropTypes.elementType working (https://git.io/fjMLX). // PropTypes.elementType is currently `undefined` in the browser. -function elementType(props, propName, componentName) { +const elementType = wrapPrimitivePropType(function(props, propName, componentName) { if (props[propName] && !isValidElementType(props[propName])) { return new Error( `Invalid prop '${propName}' supplied to '${componentName}': the prop is not a valid React component` ) } -} +}, 'elementType') export default elementType From 28448ecf40f2118a805003949b3182acbd568ee9 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Wed, 15 Apr 2020 13:33:46 -0700 Subject: [PATCH 4/5] Show generated props on selected pages --- docs/content/BorderBox.md | 11 +++++++++-- docs/content/Box.md | 11 ++++------- docs/content/Link.md | 14 ++++---------- docs/content/SideNav.md | 26 ++++++-------------------- 4 files changed, 23 insertions(+), 39 deletions(-) diff --git a/docs/content/BorderBox.md b/docs/content/BorderBox.md index 287115e210f..41352e17c24 100644 --- a/docs/content/BorderBox.md +++ b/docs/content/BorderBox.md @@ -11,7 +11,14 @@ BorderBox is a Box component with a border. When no `borderColor` is present, th This is a BorderBox ``` -## System props +## Props + +import {BorderBox} from "@primer/components" +import ComponentProps from "../components/ComponentProps" + + + + diff --git a/docs/content/Box.md b/docs/content/Box.md index e101a340098..24674edf8a7 100644 --- a/docs/content/Box.md +++ b/docs/content/Box.md @@ -16,12 +16,9 @@ The Box component serves as a wrapper component for most layout related needs. U ``` -## System props +## Props -Box components get the `COMMON` and `LAYOUT` categories of system props. Read our [System Props](/system-props) doc page for a full list of available props. +import {Box} from "@primer/components" +import ComponentProps from "../components/ComponentProps" -## Component props - -| Prop name | Type | Default | Description | -| :- | :- | :-: | :- | -| as | String | `div` | sets the HTML tag for the component| + diff --git a/docs/content/Link.md b/docs/content/Link.md index 25b302705f9..dd947f53490 100644 --- a/docs/content/Link.md +++ b/docs/content/Link.md @@ -16,15 +16,9 @@ In special cases where you'd like a `