diff --git a/.eslintrc.js b/.eslintrc.js index 8bd9c7d9346f..1b2777ad1b9c 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -17,6 +17,14 @@ module.exports = { 'import/no-extraneous-dependencies': 'off', }, }, + { + files: ['**/__testfixtures__/**'], + rules: { + 'react/forbid-prop-types': 'off', + 'react/no-unused-prop-types': 'off', + 'react/require-default-props': 'off', + }, + }, { files: '**/.storybook/config.js', rules: { 'global-require': 'off' } }, { files: ['**/*.stories.*'], diff --git a/addons/docs/docs/props-tables.md b/addons/docs/docs/props-tables.md index 9240a2c2ec98..18145e0e9231 100644 --- a/addons/docs/docs/props-tables.md +++ b/addons/docs/docs/props-tables.md @@ -7,10 +7,16 @@ Storybook Docs automatically generates props tables for components in supported frameworks. This document is a consolidated summary of prop tables, provides instructions for reporting bugs, and list known limitations for each framework. - [Usage](#usage) +- [Args Controls](#args-controls) + - [DocsPage](#docspage) + - [MDX](#mdx) + - [Controls customization](#controls-customization) + - [Rows customization](#rows-customization) - [Reporting a bug](#reporting-a-bug) - [Known limitations](#known-limitations) - [React](#react) - [Fully support React.FC](#fully-support-reactfc) + - [Imported types](#imported-types) - [Vue](#vue) - [Angular](#angular) - [Web components](#web-components) @@ -46,6 +52,127 @@ import { MyComponent } from './MyComponent'; ``` +## Args Controls + +Starting in SB 6.0, the `Props` block has built-in controls (formerly known as "knobs") for editing stories dynamically. + +
+ +
+ +These controls are implemented appear automatically in the props table when your story accepts [Storybook Args](#https://github.com/storybookjs/storybook/blob/next/docs/src/pages/formats/component-story-format/index.md#args-story-inputs) as its input. + +### DocsPage + +In DocsPage, simply write your story to consume args and the auto-generated props table will display controls in the right-most column: + +```js +export default { + title: 'MyComponent', + component: MyComponent, +}; + +export const Controls = (args) => ; +``` + +These controls can be [customized](#controls-customization) if the defaults don't meet your needs. + +### MDX + +In [MDX](./mdx.md), the `Props` controls are more configurable than in DocsPage. In order to show controls, `Props` must be a function of a story, not a component: + +```js + + {args => } + + + +``` + +### Controls customization + +Under the hood, props tables are rendered from an internal data structure called `ArgTypes`. When you declare a story's `component` metadata, Docs automatically extracts `ArgTypes` based on the component's properties. We can customize this by editing the `argTypes` metadata. + +For example, consider a `Label` component that accepts a `background` color: + +```js +import React from 'react'; +import PropTypes from 'prop-types'; + +export const Label = ({ label, borderWidth, background }) =>
{label}
; +Label.propTypes = { + label: PropTypes.string; + borderWidth: PropTypes.number; + background: PropTypes.string; +} +``` + +Given this input, the Docs addon will show a text editor for the `background` and a numeric input for the `borderWidth` prop: + +
+ +
+ +But suppose we prefer to show a color picker for `background` and a numeric input for `borderWidth`. We can customize this in the story metadata's `argTypes` field (at the component OR story level): + +```js +export default { + title: 'Label', + component: Label, + argTypes: { + background: { control: { type: 'color' } }, + borderWidth: { control: { type: 'range', min: 0, max: 6 } }, + }, +}; +``` + +This generates the following custom UI: + +
+ +
+ +Support controls include `array`, `boolean`, `color`, `date`, `range`, `object`, `text`, as well as a number of different options controls: `radio`, `inline-radio`, `check`, `inline-check`, `select`, `multi-select`. + +To see the full list of configuration options, see the [typescript type defintions](https://github.com/storybookjs/storybook/blob/next/lib/components/src/controls/types.ts). + +### Rows customization + +In addition to customizing [controls](#controls-customization), it's also possible to customize `Props` fields, such as description, or even the rows themselves. + +Consider the following story for the `Label` component from in the previous section: + +```js +export const Batch = ({ labels, padding }) => ( +
+ {labels.map((label) => ( +
+); +``` + +In this case, the args are basically unrelated to the underlying component's props, and are instead related to the individual story. To generate a prop table for the story, you can configure the Story's metadata: + +```js +Batch.story = { + argTypes: { + labels: { + description: 'A comma-separated list of labels to display', + defaultValue: 'a,b,c', + control: { type: 'array' } + } + padding: { + description: 'The padding to space out labels int he story', + defaultValue: 4, + control: { type: 'range', min: 0, max: 20, step: 2 }, + } + } +} +``` + +In this case, the user-specified `argTypes` are not a subset of the component's props, so Storybook shows ONLY the user-specified `argTypes`, and shows the component's props (without controls) in a separate tab. + ## Reporting a bug Extracting component properties from source is a tricky problem with thousands of corner cases. We've designed this package and its tests to accurately isolate problems, since the cause could either be in this package or (likely) one of the packages it depends on. diff --git a/addons/docs/src/blocks/DocsPage.tsx b/addons/docs/src/blocks/DocsPage.tsx index 4eb903794f43..d470ff761ed8 100644 --- a/addons/docs/src/blocks/DocsPage.tsx +++ b/addons/docs/src/blocks/DocsPage.tsx @@ -12,7 +12,7 @@ export const DocsPage: FC = () => ( - + ); diff --git a/addons/docs/src/blocks/Props.tsx b/addons/docs/src/blocks/Props.tsx index b696af189f3b..d6c2f40eaf71 100644 --- a/addons/docs/src/blocks/Props.tsx +++ b/addons/docs/src/blocks/Props.tsx @@ -1,83 +1,98 @@ -import React, { FunctionComponent, useContext } from 'react'; - +/* eslint-disable no-underscore-dangle */ +import React, { FC, useContext, useEffect, useState, useCallback } from 'react'; +import mapValues from 'lodash/mapValues'; import { - PropsTable, - PropsTableError, - PropsTableProps, - PropsTableRowsProps, - PropsTableSectionsProps, - PropDef, - TabsState, + ArgsTable, + ArgsTableProps, + ArgsTableError, + ArgTypes, + TabbedArgsTable, } from '@storybook/components'; +import { Args } from '@storybook/addons'; +import { StoryStore } from '@storybook/client-api'; +import Events from '@storybook/core-events'; + import { DocsContext, DocsContextProps } from './DocsContext'; import { Component, CURRENT_SELECTION } from './types'; import { getComponentName } from './utils'; +import { ArgTypesExtractor } from '../lib/docgen/types'; +import { lookupStoryId } from './Story'; -import { PropsExtractor } from '../lib/docgen/types'; -import { extractProps as reactExtractProps } from '../frameworks/react/extractProps'; -import { extractProps as vueExtractProps } from '../frameworks/vue/extractProps'; - -interface PropsProps { +interface BaseProps { exclude?: string[]; - of?: '.' | Component; - components?: { +} + +type OfProps = BaseProps & { + of: '.' | Component; +}; + +type ComponentsProps = BaseProps & { + components: { [label: string]: Component; }; -} +}; -// FIXME: remove in SB6.0 & require config -const inferPropsExtractor = (framework: string): PropsExtractor | null => { - switch (framework) { - case 'react': - return reactExtractProps; - case 'vue': - return vueExtractProps; - default: - return null; - } +type StoryProps = BaseProps & { + story: '.' | string; + showComponents?: boolean; }; -const filterRows = (rows: PropDef[], exclude: string[]) => - rows && rows.filter((row: PropDef) => !exclude.includes(row.name)); +type PropsProps = BaseProps | OfProps | ComponentsProps | StoryProps; -export const getComponentProps = ( - component: Component, - { exclude }: PropsProps, - { parameters }: DocsContextProps -): PropsTableProps => { - if (!component) { - return null; +const useArgs = (storyId: string, storyStore: StoryStore): [Args, (args: Args) => void] => { + const story = storyStore.fromId(storyId); + if (!story) { + throw new Error(`Unknown story: ${storyId}`); } - try { - const params = parameters || {}; - const { framework = null } = params; - const { extractProps = inferPropsExtractor(framework) }: { extractProps: PropsExtractor } = - params.docs || {}; - if (!extractProps) { - throw new Error(PropsTableError.PROPS_UNSUPPORTED); - } - let props = extractProps(component); - if (exclude != null) { - const { rows } = props as PropsTableRowsProps; - const { sections } = props as PropsTableSectionsProps; - if (rows) { - props = { rows: filterRows(rows, exclude) }; - } else if (sections) { - Object.keys(sections).forEach((section) => { - sections[section] = filterRows(sections[section], exclude); - }); + const { args: initialArgs } = story; + const [args, setArgs] = useState(initialArgs); + useEffect(() => { + const cb = (changedId: string, newArgs: Args) => { + if (changedId === storyId) { + setArgs(newArgs); } - } + }; + storyStore._channel.on(Events.STORY_ARGS_UPDATED, cb); + return () => storyStore._channel.off(Events.STORY_ARGS_UPDATED, cb); + }, [storyId]); + const updateArgs = useCallback((newArgs) => storyStore.updateStoryArgs(storyId, newArgs), [ + storyId, + ]); + return [args, updateArgs]; +}; - return props; - } catch (err) { - return { error: err.message }; +const filterArgTypes = (argTypes: ArgTypes, exclude?: string[]) => { + if (!exclude) { + return argTypes; } + return ( + argTypes && + mapValues(argTypes, (argType, key) => { + const name = argType.name || key; + return exclude.includes(name) ? undefined : argType; + }) + ); +}; + +export const extractComponentArgTypes = ( + component: Component, + { parameters }: DocsContextProps, + exclude?: string[] +): ArgTypes => { + const params = parameters || {}; + const { extractArgTypes }: { extractArgTypes: ArgTypesExtractor } = params.docs || {}; + if (!extractArgTypes) { + throw new Error(ArgsTableError.ARGS_UNSUPPORTED); + } + let argTypes = extractArgTypes(component); + argTypes = filterArgTypes(argTypes, exclude); + + return argTypes; }; export const getComponent = (props: PropsProps = {}, context: DocsContextProps): Component => { - const { of } = props; + const { of } = props as OfProps; const { parameters = {} } = context; const { component } = parameters; @@ -86,60 +101,100 @@ export const getComponent = (props: PropsProps = {}, context: DocsContextProps): if (of === CURRENT_SELECTION) { return null; } - throw new Error(PropsTableError.NO_COMPONENT); + throw new Error(ArgsTableError.NO_COMPONENT); } return target; }; -const PropsContainer: FunctionComponent = (props) => { +const addComponentTabs = ( + tabs: Record, + components: Record, + context: DocsContextProps, + exclude?: string[] +) => ({ + ...tabs, + ...mapValues(components, (comp) => ({ + rows: extractComponentArgTypes(comp, context, exclude), + })), +}); + +export const StoryTable: FC }> = (props) => { + const context = useContext(DocsContext); + const { + id: currentId, + parameters: { argTypes }, + storyStore, + } = context; + const { story, showComponents, components, exclude } = props; + let storyArgTypes; + try { + let storyId; + if (story === CURRENT_SELECTION) { + storyId = currentId; + storyArgTypes = argTypes; + } else { + storyId = lookupStoryId(story, context); + const data = storyStore.fromId(storyId); + storyArgTypes = data.parameters.argTypes; + } + storyArgTypes = filterArgTypes(storyArgTypes, exclude); + const [args, updateArgs] = useArgs(storyId, storyStore); + let tabs = { Story: { rows: storyArgTypes, args, updateArgs } } as Record< + string, + ArgsTableProps + >; + if (showComponents) { + tabs = addComponentTabs(tabs, components, context, exclude); + } + + return ; + } catch (err) { + return ; + } +}; + +export const ComponentsTable: FC = (props) => { + const context = useContext(DocsContext); + const { components, exclude } = props; + + const tabs = addComponentTabs({}, components, context, exclude); + return ; +}; + +export const Props: FC = (props) => { const context = useContext(DocsContext); - const { components } = props; const { parameters: { subcomponents }, } = context; - let allComponents = components; - if (!allComponents) { - const main = getComponent(props, context); - const mainLabel = getComponentName(main); - const mainProps = getComponentProps(main, props, context); + const { exclude, components } = props as ComponentsProps; + const { story } = props as StoryProps; - if (!subcomponents || typeof subcomponents !== 'object') { - return mainProps && ; - } + let allComponents = components; + const main = getComponent(props, context); + if (!allComponents && main) { + const mainLabel = getComponentName(main); allComponents = { [mainLabel]: main, ...subcomponents }; } - const tabs: { label: string; table: PropsTableProps }[] = []; - Object.entries(allComponents).forEach(([label, component]) => { - tabs.push({ - label, - table: getComponentProps(component, props, context), - }); - }); + if (story) { + return ; + } + + if (!components && !subcomponents) { + let mainProps; + try { + mainProps = { rows: extractComponentArgTypes(main, context, exclude) }; + } catch (err) { + mainProps = { error: err.message }; + } + return ; + } - return ( - - {tabs.map(({ label, table }) => { - if (!table) { - return null; - } - const id = `prop_table_div_${label}`; - return ( -
- {({ active }: { active: boolean }) => - active ? : null - } -
- ); - })} -
- ); + return ; }; -PropsContainer.defaultProps = { - of: '.', +Props.defaultProps = { + of: CURRENT_SELECTION, }; - -export { PropsContainer as Props }; diff --git a/addons/docs/src/blocks/Source.tsx b/addons/docs/src/blocks/Source.tsx index 91e7cb90c4fd..ec7c624c1def 100644 --- a/addons/docs/src/blocks/Source.tsx +++ b/addons/docs/src/blocks/Source.tsx @@ -40,7 +40,7 @@ export const getSourceProps = ( source = targetIds .map((sourceId) => { const data = storyStore.fromId(sourceId); - const enhanced = enhanceSource(data); + const enhanced = data && enhanceSource(data); return enhanced?.docs?.source?.code || ''; }) .join('\n\n'); diff --git a/addons/docs/src/blocks/Story.tsx b/addons/docs/src/blocks/Story.tsx index c32a0da4b8dc..0223ad068e84 100644 --- a/addons/docs/src/blocks/Story.tsx +++ b/addons/docs/src/blocks/Story.tsx @@ -1,7 +1,7 @@ import React, { createElement, ElementType, FunctionComponent, ReactNode } from 'react'; import { MDXProvider } from '@mdx-js/react'; import { components as docsComponents } from '@storybook/components/html'; -import { Story, StoryProps as PureStoryProps } from '@storybook/components'; +import { Story as PureStory, StoryProps as PureStoryProps } from '@storybook/components'; import { toId, storyNameFromExport } from '@storybook/csf'; import { CURRENT_SELECTION } from './types'; @@ -39,22 +39,23 @@ const inferInlineStories = (framework: string): boolean => { } }; -export const getStoryProps = ( - props: StoryProps, - { id: currentId, storyStore, mdxStoryNameToKey, mdxComponentMeta }: DocsContextProps | null -): PureStoryProps => { +export const lookupStoryId = ( + storyName: string, + { mdxStoryNameToKey, mdxComponentMeta }: DocsContextProps +) => + toId( + mdxComponentMeta.id || mdxComponentMeta.title, + storyNameFromExport(mdxStoryNameToKey[storyName]) + ); + +export const getStoryProps = (props: StoryProps, context: DocsContextProps): PureStoryProps => { const { id } = props as StoryRefProps; const { name } = props as StoryDefProps; - const inputId = id === CURRENT_SELECTION ? currentId : id; - const previewId = - inputId || - toId( - mdxComponentMeta.id || mdxComponentMeta.title, - storyNameFromExport(mdxStoryNameToKey[name]) - ); + const inputId = id === CURRENT_SELECTION ? context.id : id; + const previewId = inputId || lookupStoryId(name, context); const { height, inline } = props; - const data = storyStore.fromId(previewId); + const data = context.storyStore.fromId(previewId); const { framework = null } = (data && data.parameters) || {}; const docsParam = (data && data.parameters && data.parameters.docs) || {}; @@ -87,7 +88,7 @@ export const getStoryProps = ( }; }; -const StoryContainer: FunctionComponent = (props) => ( +const Story: FunctionComponent = (props) => ( {(context) => { const storyProps = getStoryProps(props, context); @@ -97,7 +98,7 @@ const StoryContainer: FunctionComponent = (props) => ( return (
- +
); @@ -105,9 +106,9 @@ const StoryContainer: FunctionComponent = (props) => (
); -StoryContainer.defaultProps = { +Story.defaultProps = { children: null, name: null, }; -export { StoryContainer as Story }; +export { Story }; diff --git a/addons/docs/src/frameworks/angular/__testfixtures__/doc-button/argtypes.snapshot b/addons/docs/src/frameworks/angular/__testfixtures__/doc-button/argtypes.snapshot new file mode 100644 index 000000000000..82765f71b3b8 --- /dev/null +++ b/addons/docs/src/frameworks/angular/__testfixtures__/doc-button/argtypes.snapshot @@ -0,0 +1,272 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`angular component properties doc-button 1`] = ` +Object { + "_inputValue": Object { + "defaultValue": Object { + "summary": "'some value'", + }, + "description": "", + "name": "_inputValue", + "table": Object { + "category": "properties", + "type": Object { + "required": true, + "summary": "string", + }, + }, + }, + "_value": Object { + "defaultValue": Object { + "summary": "'Private hello'", + }, + "description": "

Private value.

+", + "name": "_value", + "table": Object { + "category": "properties", + "type": Object { + "required": true, + "summary": "string", + }, + }, + }, + "appearance": Object { + "defaultValue": Object { + "summary": "'secondary'", + }, + "description": "

Appearance style of the button.

+", + "name": "appearance", + "table": Object { + "category": "inputs", + "type": Object { + "required": true, + "summary": "\\"primary\\" | \\"secondary\\"", + }, + }, + }, + "buttonRef": Object { + "defaultValue": Object { + "summary": undefined, + }, + "description": "", + "name": "buttonRef", + "table": Object { + "category": "view child", + "type": Object { + "required": true, + "summary": "ElementRef", + }, + }, + }, + "calc": Object { + "defaultValue": Object { + "summary": "", + }, + "description": "

An internal calculation method which adds x and y together.

+", + "name": "calc", + "table": Object { + "category": "methods", + "type": Object { + "required": false, + "summary": "(x: number, y: string | number) => number", + }, + }, + }, + "inputValue": Object { + "defaultValue": Object { + "summary": undefined, + }, + "description": "

Setter for inputValue that is also an @Input.

+", + "name": "inputValue", + "table": Object { + "category": "inputs", + "type": Object { + "required": true, + "summary": "string", + }, + }, + }, + "internalProperty": Object { + "defaultValue": Object { + "summary": "'Public hello'", + }, + "description": "

Public value.

+", + "name": "internalProperty", + "table": Object { + "category": "properties", + "type": Object { + "required": true, + "summary": "string", + }, + }, + }, + "isDisabled": Object { + "defaultValue": Object { + "summary": "false", + }, + "description": "

Sets the button to a disabled state.

+", + "name": "isDisabled", + "table": Object { + "category": "inputs", + "type": Object { + "required": true, + "summary": undefined, + }, + }, + }, + "item": Object { + "defaultValue": Object { + "summary": undefined, + }, + "description": undefined, + "name": "item", + "table": Object { + "category": "inputs", + "type": Object { + "required": true, + "summary": "[]", + }, + }, + }, + "label": Object { + "defaultValue": Object { + "summary": undefined, + }, + "description": "

The inner text of the button.

+", + "name": "label", + "table": Object { + "category": "inputs", + "type": Object { + "required": true, + "summary": "string", + }, + }, + }, + "onClick": Object { + "defaultValue": Object { + "summary": "new EventEmitter()", + }, + "description": "

Handler to be called when the button is clicked by a user.

+

Will also block the emission of the event if isDisabled is true.

+", + "name": "onClick", + "table": Object { + "category": "outputs", + "type": Object { + "required": true, + "summary": "EventEmitter", + }, + }, + }, + "privateMethod": Object { + "defaultValue": Object { + "summary": "", + }, + "description": "

A private method.

+", + "name": "privateMethod", + "table": Object { + "category": "methods", + "type": Object { + "required": false, + "summary": "(password: string) => void", + }, + }, + }, + "processedItem": Object { + "defaultValue": Object { + "summary": undefined, + }, + "description": "", + "name": "processedItem", + "table": Object { + "category": "properties", + "type": Object { + "required": true, + "summary": "T[]", + }, + }, + }, + "protectedMethod": Object { + "defaultValue": Object { + "summary": "", + }, + "description": "

A protected method.

+", + "name": "protectedMethod", + "table": Object { + "category": "methods", + "type": Object { + "required": false, + "summary": "(id?: number) => void", + }, + }, + }, + "publicMethod": Object { + "defaultValue": Object { + "summary": "", + }, + "description": "

A public method using an interface.

+", + "name": "publicMethod", + "table": Object { + "category": "methods", + "type": Object { + "required": false, + "summary": "(things: ISomeInterface) => void", + }, + }, + }, + "showKeyAlias": Object { + "defaultValue": Object { + "summary": undefined, + }, + "description": undefined, + "name": "showKeyAlias", + "table": Object { + "category": "inputs", + "type": Object { + "required": true, + "summary": "", + }, + }, + }, + "size": Object { + "defaultValue": Object { + "summary": "'medium'", + }, + "description": "

Size of the button.

+", + "name": "size", + "table": Object { + "category": "inputs", + "type": Object { + "required": true, + "summary": "ButtonSize", + }, + }, + }, + "somethingYouShouldNotUse": Object { + "defaultValue": Object { + "summary": "false", + }, + "description": "

Some input you shouldn't use.

+", + "name": "somethingYouShouldNotUse", + "table": Object { + "category": "inputs", + "type": Object { + "required": true, + "summary": undefined, + }, + }, + }, +} +`; diff --git a/addons/docs/src/frameworks/angular/angular-properties.test.ts b/addons/docs/src/frameworks/angular/angular-properties.test.ts index 1cf134abc176..b1d1eefe50cf 100644 --- a/addons/docs/src/frameworks/angular/angular-properties.test.ts +++ b/addons/docs/src/frameworks/angular/angular-properties.test.ts @@ -4,7 +4,7 @@ import fs from 'fs'; import tmp from 'tmp'; import { sync as spawnSync } from 'cross-spawn'; -import { findComponentByName, extractPropsFromData } from './compodoc'; +import { findComponentByName, extractArgTypesFromData } from './compodoc'; // File hierarchy: __testfixtures__ / some-test-case / input.* const inputRegExp = /^input\..*$/; @@ -41,8 +41,8 @@ describe('angular component properties', () => { // snapshot the output of addon-docs angular-properties const componentData = findComponentByName('InputComponent', compodocJson); - const properties = extractPropsFromData(componentData); - expect(properties).toMatchSpecificSnapshot(path.join(testDir, 'properties.snapshot')); + const argTypes = extractArgTypesFromData(componentData); + expect(argTypes).toMatchSpecificSnapshot(path.join(testDir, 'argtypes.snapshot')); }); } } diff --git a/addons/docs/src/frameworks/angular/compodoc.ts b/addons/docs/src/frameworks/angular/compodoc.ts index 7571664abfad..fc0938b14223 100644 --- a/addons/docs/src/frameworks/angular/compodoc.ts +++ b/addons/docs/src/frameworks/angular/compodoc.ts @@ -2,6 +2,7 @@ /* global window */ import { PropDef } from '@storybook/components'; +import { ArgType, ArgTypes } from '@storybook/api'; import { Argument, CompodocJson, Component, Method, Property, Directive } from './types'; type Sections = Record; @@ -30,10 +31,6 @@ export const checkValidCompodocJson = (compodocJson: CompodocJson) => { } }; -function isEmpty(obj: any) { - return Object.entries(obj).length === 0 && obj.constructor === Object; -} - const hasDecorator = (item: Property, decoratorName: string) => item.decorators && item.decorators.find((x: any) => x.name === decoratorName); @@ -93,31 +90,35 @@ const displaySignature = (item: Method): string => { return `(${args.join(', ')}) => ${item.returnType}`; }; -export const extractPropsFromData = (componentData: Directive) => { - const sectionToItems: Sections = {}; +export const extractArgTypesFromData = (componentData: Directive) => { + const sectionToItems: Record = {}; const compodocClasses = ['propertiesClass', 'methodsClass', 'inputsClass', 'outputsClass']; type COMPODOC_CLASS = 'propertiesClass' | 'methodsClass' | 'inputsClass' | 'outputsClass'; compodocClasses.forEach((key: COMPODOC_CLASS) => { const data = componentData[key] || []; data.forEach((item: Method | Property) => { - const sectionItem: PropDef = { + const section = mapItemToSection(key, item); + const argType = { name: item.name, - type: { summary: isMethod(item) ? displaySignature(item) : item.type }, - required: isMethod(item) ? false : !item.optional, description: item.description, defaultValue: { summary: isMethod(item) ? '' : item.defaultValue }, + table: { + category: section, + type: { + summary: isMethod(item) ? displaySignature(item) : item.type, + required: isMethod(item) ? false : !item.optional, + }, + }, }; - const section = mapItemToSection(key, item); if (!sectionToItems[section]) { sectionToItems[section] = []; } - sectionToItems[section].push(sectionItem); + sectionToItems[section].push(argType); }); }); - // sort the sections const SECTIONS = [ 'inputs', 'outputs', @@ -128,20 +129,22 @@ export const extractPropsFromData = (componentData: Directive) => { 'content child', 'content children', ]; - const sections: Sections = {}; + const argTypes: ArgTypes = {}; SECTIONS.forEach((section) => { const items = sectionToItems[section]; if (items) { - sections[section] = items; + items.forEach((argType) => { + argTypes[argType.name] = argType; + }); } }); - return isEmpty(sections) ? null : { sections }; + return argTypes; }; -export const extractProps = (component: Component | Directive) => { +export const extractArgTypes = (component: Component | Directive) => { const componentData = getComponentData(component); - return componentData && extractPropsFromData(componentData); + return componentData && extractArgTypesFromData(componentData); }; export const extractComponentDescription = (component: Component | Directive) => { diff --git a/addons/docs/src/frameworks/angular/config.ts b/addons/docs/src/frameworks/angular/config.ts index 7759ad0c09f6..dc06b77c8d37 100644 --- a/addons/docs/src/frameworks/angular/config.ts +++ b/addons/docs/src/frameworks/angular/config.ts @@ -1,9 +1,9 @@ import { addParameters } from '@storybook/client-api'; -import { extractProps, extractComponentDescription } from './compodoc'; +import { extractArgTypes, extractComponentDescription } from './compodoc'; addParameters({ docs: { - extractProps, + extractArgTypes, extractComponentDescription, }, }); diff --git a/addons/docs/src/frameworks/common/config.ts b/addons/docs/src/frameworks/common/config.ts index b9437f2f6238..54ea1d2235d6 100644 --- a/addons/docs/src/frameworks/common/config.ts +++ b/addons/docs/src/frameworks/common/config.ts @@ -1,5 +1,6 @@ /* eslint-disable-next-line import/no-extraneous-dependencies */ import { DocsPage, DocsContainer } from '@storybook/addon-docs/blocks'; +import { enhanceArgTypes } from './enhanceArgTypes'; export const parameters = { docs: { @@ -7,3 +8,5 @@ export const parameters = { page: DocsPage, }, }; + +export const argTypesEnhancers = [enhanceArgTypes]; diff --git a/addons/docs/src/frameworks/common/enhanceArgTypes.test.ts b/addons/docs/src/frameworks/common/enhanceArgTypes.test.ts new file mode 100644 index 000000000000..889a5b296be4 --- /dev/null +++ b/addons/docs/src/frameworks/common/enhanceArgTypes.test.ts @@ -0,0 +1,318 @@ +import { ArgType, ArgTypes, Args } from '@storybook/api'; +import { enhanceArgTypes } from './enhanceArgTypes'; + +expect.addSnapshotSerializer({ + print: (val: any) => JSON.stringify(val, null, 2), + test: (val) => typeof val !== 'string', +}); + +const enhance = ({ + argType, + arg, + extractedArgTypes, + storyFn = (args: Args) => 0, +}: { + argType?: ArgType; + arg?: any; + extractedArgTypes?: ArgTypes; + storyFn?: any; +}) => { + const context = { + id: 'foo--bar', + kind: 'foo', + name: 'bar', + storyFn, + parameters: { + component: 'dummy', + docs: { + extractArgTypes: extractedArgTypes && (() => extractedArgTypes), + }, + argTypes: argType && { + input: argType, + }, + args: { + input: arg, + }, + }, + args: {}, + globalArgs: {}, + }; + return enhanceArgTypes(context); +}; + +describe('enhanceArgTypes', () => { + describe('no-args story function', () => { + it('should no-op', () => { + expect( + enhance({ + argType: { foo: 'unmodified', type: { name: 'number' } }, + storyFn: () => 0, + }).input + ).toMatchInlineSnapshot(` + { + "name": "input", + "foo": "unmodified", + "type": { + "name": "number" + } + } + `); + }); + }); + describe('args story function', () => { + describe('single-source input', () => { + describe('argTypes input', () => { + it('number', () => { + expect( + enhance({ + argType: { type: { name: 'number' } }, + }).input + ).toMatchInlineSnapshot(` + { + "control": { + "type": "number" + }, + "name": "input", + "type": { + "name": "number" + } + } + `); + }); + }); + + describe('args input', () => { + it('number', () => { + expect(enhance({ arg: 5 }).input).toMatchInlineSnapshot(` + { + "control": { + "type": "number" + }, + "name": "input", + "type": { + "name": "number" + } + } + `); + }); + }); + + describe('extraction from component', () => { + it('number', () => { + expect( + enhance({ extractedArgTypes: { input: { name: 'input', type: { name: 'number' } } } }) + .input + ).toMatchInlineSnapshot(` + { + "control": { + "type": "number" + }, + "name": "input", + "type": { + "name": "number" + } + } + `); + }); + }); + + describe('controls input', () => { + it('range', () => { + expect( + enhance({ + argType: { control: { type: 'range', min: 0, max: 100 } }, + }).input + ).toMatchInlineSnapshot(` + { + "name": "input", + "control": { + "type": "range", + "min": 0, + "max": 100 + } + } + `); + }); + it('options', () => { + expect( + enhance({ + argType: { control: { type: 'options', options: [1, 2], controlType: 'radio' } }, + }).input + ).toMatchInlineSnapshot(` + { + "name": "input", + "control": { + "type": "options", + "options": [ + 1, + 2 + ], + "controlType": "radio" + } + } + `); + }); + }); + }); + + describe('mixed-source input', () => { + it('user-specified argTypes take precedence over extracted argTypes', () => { + expect( + enhance({ + argType: { type: { name: 'number' } }, + extractedArgTypes: { input: { type: { name: 'string' } } }, + }).input + ).toMatchInlineSnapshot(` + { + "control": { + "type": "number" + }, + "type": { + "name": "number" + }, + "name": "input" + } + `); + }); + + it('user-specified argTypes take precedence over inferred argTypes', () => { + expect( + enhance({ + argType: { type: { name: 'number' } }, + arg: 'hello', + }).input + ).toMatchInlineSnapshot(` + { + "control": { + "type": "number" + }, + "name": "input", + "type": { + "name": "number" + } + } + `); + }); + + it('extracted argTypes take precedence over inferred argTypes', () => { + expect( + enhance({ + extractedArgTypes: { input: { type: { name: 'string' } } }, + arg: 6, + }).input + ).toMatchInlineSnapshot(` + { + "control": { + "type": "text" + }, + "name": "input", + "type": { + "name": "string" + } + } + `); + }); + + it('user-specified controls take precedence over inferred controls', () => { + expect( + enhance({ + argType: { defaultValue: 5, control: { type: 'range', step: 50 } }, + arg: 3, + extractedArgTypes: { input: { name: 'input' } }, + }).input + ).toMatchInlineSnapshot(` + { + "control": { + "type": "range", + "step": 50 + }, + "name": "input", + "type": { + "name": "number" + }, + "defaultValue": 5 + } + `); + }); + + it('includes extracted argTypes when there are no user-specified argTypes', () => { + expect( + enhance({ + arg: 3, + extractedArgTypes: { input: { name: 'input' }, foo: { type: { name: 'number' } } }, + }) + ).toMatchInlineSnapshot(` + { + "input": { + "control": { + "type": "number" + }, + "name": "input", + "type": { + "name": "number" + } + }, + "foo": { + "control": { + "type": "number" + }, + "type": { + "name": "number" + } + } + } + `); + }); + + it('includes extracted argTypes when user-specified argTypes match', () => { + expect( + enhance({ + argType: { type: { name: 'number' } }, + extractedArgTypes: { input: { name: 'input' }, foo: { type: { name: 'number' } } }, + }) + ).toMatchInlineSnapshot(` + { + "input": { + "control": { + "type": "number" + }, + "name": "input", + "type": { + "name": "number" + } + }, + "foo": { + "control": { + "type": "number" + }, + "type": { + "name": "number" + } + } + } + `); + }); + + it('excludes extracted argTypes when user-specified argTypes do not match', () => { + expect( + enhance({ + argType: { type: { name: 'number' } }, + extractedArgTypes: { foo: { type: { name: 'number' } } }, + }) + ).toMatchInlineSnapshot(` + { + "input": { + "control": { + "type": "number" + }, + "name": "input", + "type": { + "name": "number" + } + } + } + `); + }); + }); + }); +}); diff --git a/addons/docs/src/frameworks/common/enhanceArgTypes.ts b/addons/docs/src/frameworks/common/enhanceArgTypes.ts new file mode 100644 index 000000000000..eccad255e8fa --- /dev/null +++ b/addons/docs/src/frameworks/common/enhanceArgTypes.ts @@ -0,0 +1,61 @@ +import mapValues from 'lodash/mapValues'; +import { ArgTypesEnhancer, combineParameters } from '@storybook/client-api'; +import { ArgTypes } from '@storybook/api'; +import { inferArgTypes } from './inferArgTypes'; +import { inferControls } from './inferControls'; + +const isSubset = (kind: string, subset: object, superset: object) => { + const keys = Object.keys(subset); + // eslint-disable-next-line no-prototype-builtins + const overlap = keys.filter((key) => superset.hasOwnProperty(key)); + return overlap.length === keys.length; +}; + +export const enhanceArgTypes: ArgTypesEnhancer = (context) => { + const { + component, + subcomponents, + argTypes: userArgTypes = {}, + docs = {}, + args = {}, + } = context.parameters; + const { extractArgTypes } = docs; + + const namedArgTypes = mapValues(userArgTypes, (val, key) => ({ name: key, ...val })); + const inferredArgTypes = inferArgTypes(args); + const components = { Primary: component, ...subcomponents }; + let extractedArgTypes: ArgTypes = {}; + + if (extractArgTypes && components) { + const componentArgTypes = mapValues(components, (comp) => extractArgTypes(comp)); + extractedArgTypes = Object.entries(componentArgTypes).reduce((acc, [label, compTypes]) => { + if (compTypes) { + Object.entries(compTypes).forEach(([key, argType]) => { + if (label === 'Primary') { + acc[key] = argType; + } + }); + } + return acc; + }, {} as ArgTypes); + } + + if ( + (Object.keys(userArgTypes).length > 0 && + !isSubset(context.kind, userArgTypes, extractedArgTypes)) || + (Object.keys(inferredArgTypes).length > 0 && + !isSubset(context.kind, inferredArgTypes, extractedArgTypes)) + ) { + extractedArgTypes = {}; + } + + const withArgTypes = combineParameters(inferredArgTypes, extractedArgTypes, namedArgTypes); + + if (context.storyFn.length === 0) { + return withArgTypes; + } + + const withControls = inferControls(withArgTypes); + const result = combineParameters(withControls, withArgTypes); + return result; +}; diff --git a/addons/docs/src/frameworks/common/inferArgTypes.ts b/addons/docs/src/frameworks/common/inferArgTypes.ts new file mode 100644 index 000000000000..1fb5427948ec --- /dev/null +++ b/addons/docs/src/frameworks/common/inferArgTypes.ts @@ -0,0 +1,36 @@ +import mapValues from 'lodash/mapValues'; +import { Args, ArgTypes } from '@storybook/addons'; +import { SBType } from '../../lib/sbtypes'; + +const inferType = (value?: any): SBType => { + const type = typeof value; + switch (type) { + case 'boolean': + case 'string': + case 'number': + case 'function': + return { name: type }; + default: + break; + } + if (Array.isArray(value)) { + const childType: SBType = + value.length > 0 ? inferType(value[0]) : { name: 'other', value: 'unknown' }; + return { name: 'array', value: [childType] }; + } + if (value) { + const fieldTypes = mapValues(value, (field) => inferType(field)); + return { name: 'object', value: fieldTypes }; + } + return { name: 'other', value: 'unknown' }; +}; + +export const inferArgTypes = (args: Args): ArgTypes => { + if (!args) return {}; + return mapValues(args, (arg, name) => { + if (arg !== null && typeof arg !== 'undefined') { + return { name, type: inferType(arg) }; + } + return undefined; + }); +}; diff --git a/addons/docs/src/frameworks/common/inferControls.ts b/addons/docs/src/frameworks/common/inferControls.ts new file mode 100644 index 000000000000..4f4a36544456 --- /dev/null +++ b/addons/docs/src/frameworks/common/inferControls.ts @@ -0,0 +1,37 @@ +import mapValues from 'lodash/mapValues'; +import { ArgTypes, ArgType } from '@storybook/addons'; +import { Control } from '@storybook/components'; +import { SBEnumType } from '../../lib/sbtypes'; + +const inferControl = (argType: ArgType): Control => { + if (!argType.type) { + // console.log('no sbtype', { argType }); + return null; + } + switch (argType.type.name) { + case 'array': + return { type: 'array' }; + case 'boolean': + return { type: 'boolean' }; + case 'string': + return { type: 'text' }; + case 'number': + return { type: 'number' }; + case 'enum': { + const { value } = argType.type as SBEnumType; + return { type: 'options', controlType: 'select', options: value }; + } + case 'function': + case 'symbol': + return null; + default: + return { type: 'object' }; + } +}; + +export const inferControls = (argTypes: ArgTypes): ArgTypes => { + return mapValues(argTypes, (argType) => { + const control = argType && argType.type && inferControl(argType); + return control ? { control } : undefined; + }); +}; diff --git a/addons/docs/src/frameworks/common/preset.ts b/addons/docs/src/frameworks/common/preset.ts index a94ce8f4b999..8378c0db7caf 100644 --- a/addons/docs/src/frameworks/common/preset.ts +++ b/addons/docs/src/frameworks/common/preset.ts @@ -4,11 +4,6 @@ import path from 'path'; import remarkSlug from 'remark-slug'; import remarkExternalLinks from 'remark-external-links'; -import { DllReferencePlugin } from 'webpack'; - -const coreDirName = path.dirname(require.resolve('@storybook/core/package.json')); -const context = path.join(coreDirName, '../../node_modules'); - function createBabelOptions(babelOptions?: any, configureJSX?: boolean) { if (!configureJSX) { return babelOptions; @@ -24,10 +19,6 @@ function createBabelOptions(babelOptions?: any, configureJSX?: boolean) { }; } -export const webpackDlls = (dlls: string[], options: any) => { - return options.dll ? [...dlls, './sb_dll/storybook_docs_dll.js'] : []; -}; - export function webpack(webpackConfig: any = {}, options: any = {}) { const { module = {} } = webpackConfig; // it will reuse babel options that are already in use in storybook @@ -106,16 +97,6 @@ export function webpack(webpackConfig: any = {}, options: any = {}) { ], }, }; - - if (options.dll) { - result.plugins.push( - new DllReferencePlugin({ - context, - manifest: path.join(coreDirName, 'dll', 'storybook_docs-manifest.json'), - }) - ); - } - return result; } diff --git a/addons/docs/src/frameworks/ember/config.js b/addons/docs/src/frameworks/ember/config.js index 0b0e9e5645b2..9745c9eca4c9 100644 --- a/addons/docs/src/frameworks/ember/config.js +++ b/addons/docs/src/frameworks/ember/config.js @@ -1,10 +1,10 @@ import { addParameters } from '@storybook/client-api'; -import { extractProps, extractComponentDescription } from './jsondoc'; +import { extractArgTypes, extractComponentDescription } from './jsondoc'; addParameters({ docs: { iframeHeight: 80, - extractProps, + extractArgTypes, extractComponentDescription, }, }); diff --git a/addons/docs/src/frameworks/ember/jsondoc.js b/addons/docs/src/frameworks/ember/jsondoc.js index d1c803a25886..307f8c35bc66 100644 --- a/addons/docs/src/frameworks/ember/jsondoc.js +++ b/addons/docs/src/frameworks/ember/jsondoc.js @@ -8,16 +8,20 @@ export const getJSONDoc = () => { return window.__EMBER_GENERATED_DOC_JSON__; }; -export const extractProps = (componentName) => { +export const extractArgTypes = (componentName) => { const json = getJSONDoc(); const componentDoc = json.included.find((doc) => doc.attributes.name === componentName); const rows = componentDoc.attributes.arguments.map((prop) => { return { name: prop.name, - type: prop.type, - required: prop.tags.length ? prop.tags.some((tag) => tag.name === 'required') : false, defaultValue: prop.defaultValue, description: prop.description, + table: { + type: { + summary: prop.type, + required: prop.tags.length ? prop.tags.some((tag) => tag.name === 'required') : false, + }, + }, }; }); return { rows }; diff --git a/addons/docs/src/frameworks/react/__testfixtures__/10017-ts-union/properties.snapshot b/addons/docs/src/frameworks/react/__testfixtures__/10017-ts-union/properties.snapshot index c0e20475596e..430e86422090 100644 --- a/addons/docs/src/frameworks/react/__testfixtures__/10017-ts-union/properties.snapshot +++ b/addons/docs/src/frameworks/react/__testfixtures__/10017-ts-union/properties.snapshot @@ -8,6 +8,20 @@ Object { "description": "specify icon=\\"search\\" or icon={IconComponent}", "name": "icon", "required": true, + "sbType": Object { + "name": "union", + "raw": "React.ReactNode | string", + "value": Array [ + Object { + "name": "other", + "raw": "React.ReactNode", + "value": "ReactReactNode", + }, + Object { + "name": "string", + }, + ], + }, "type": Object { "detail": undefined, "summary": "union", diff --git a/addons/docs/src/frameworks/react/__testfixtures__/10278-ts-multiple-components/properties.snapshot b/addons/docs/src/frameworks/react/__testfixtures__/10278-ts-multiple-components/properties.snapshot index 315d64d1b13f..f9f94045bb53 100644 --- a/addons/docs/src/frameworks/react/__testfixtures__/10278-ts-multiple-components/properties.snapshot +++ b/addons/docs/src/frameworks/react/__testfixtures__/10278-ts-multiple-components/properties.snapshot @@ -8,6 +8,10 @@ Object { "description": "", "name": "aProperty", "required": true, + "sbType": Object { + "name": "other", + "value": "any", + }, "type": Object { "detail": undefined, "summary": "any", diff --git a/addons/docs/src/frameworks/react/__testfixtures__/8140-js-prop-types-oneof/docgen.snapshot b/addons/docs/src/frameworks/react/__testfixtures__/8140-js-prop-types-oneof/docgen.snapshot index 84e7bb8527ae..0f5ed90d714b 100644 --- a/addons/docs/src/frameworks/react/__testfixtures__/8140-js-prop-types-oneof/docgen.snapshot +++ b/addons/docs/src/frameworks/react/__testfixtures__/8140-js-prop-types-oneof/docgen.snapshot @@ -1,10 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`react component properties 8140-js-prop-types-oneof 1`] = ` -"/* eslint-disable react/no-unused-prop-types */ - -/* eslint-disable react/require-default-props */ -import React from 'react'; +"import React from 'react'; import PropTypes from 'prop-types'; const Alert = props => /*#__PURE__*/React.createElement(React.Fragment, null, JSON.stringify(props)); diff --git a/addons/docs/src/frameworks/react/__testfixtures__/8140-js-prop-types-oneof/input.js b/addons/docs/src/frameworks/react/__testfixtures__/8140-js-prop-types-oneof/input.js index 64aea2d54975..1eb6b30cf299 100644 --- a/addons/docs/src/frameworks/react/__testfixtures__/8140-js-prop-types-oneof/input.js +++ b/addons/docs/src/frameworks/react/__testfixtures__/8140-js-prop-types-oneof/input.js @@ -1,5 +1,3 @@ -/* eslint-disable react/no-unused-prop-types */ -/* eslint-disable react/require-default-props */ import React from 'react'; import PropTypes from 'prop-types'; diff --git a/addons/docs/src/frameworks/react/__testfixtures__/8140-js-prop-types-oneof/properties.snapshot b/addons/docs/src/frameworks/react/__testfixtures__/8140-js-prop-types-oneof/properties.snapshot index fcd3cad8942c..ff183b161512 100644 --- a/addons/docs/src/frameworks/react/__testfixtures__/8140-js-prop-types-oneof/properties.snapshot +++ b/addons/docs/src/frameworks/react/__testfixtures__/8140-js-prop-types-oneof/properties.snapshot @@ -11,6 +11,13 @@ Object { "description": "", "name": "mode", "required": false, + "sbType": Object { + "name": "enum", + "value": Array [ + "static", + "timed", + ], + }, "type": Object { "detail": undefined, "summary": "'static' | 'timed'", @@ -24,6 +31,15 @@ Object { "description": "", "name": "type", "required": false, + "sbType": Object { + "name": "enum", + "value": Array [ + "success", + "warning", + "error", + "primary", + ], + }, "type": Object { "detail": undefined, "summary": "'success' | 'warning' | 'error' | 'primary'", @@ -34,6 +50,9 @@ Object { "description": "", "name": "message", "required": true, + "sbType": Object { + "name": "string", + }, "type": Object { "detail": undefined, "summary": "string", @@ -44,6 +63,9 @@ Object { "description": "No background or border if static alert", "name": "blank", "required": false, + "sbType": Object { + "name": "boolean", + }, "type": Object { "detail": undefined, "summary": "bool", @@ -54,6 +76,9 @@ Object { "description": "Allows icon override, accepts material icon name", "name": "icon", "required": false, + "sbType": Object { + "name": "string", + }, "type": Object { "detail": undefined, "summary": "string", diff --git a/addons/docs/src/frameworks/react/__testfixtures__/8143-ts-imported-types/properties.snapshot b/addons/docs/src/frameworks/react/__testfixtures__/8143-ts-imported-types/properties.snapshot index 667b84e751f9..e999d6a79f28 100644 --- a/addons/docs/src/frameworks/react/__testfixtures__/8143-ts-imported-types/properties.snapshot +++ b/addons/docs/src/frameworks/react/__testfixtures__/8143-ts-imported-types/properties.snapshot @@ -8,6 +8,11 @@ Object { "description": "", "name": "bar", "required": true, + "sbType": Object { + "name": "other", + "raw": "Foo['bar']", + "value": "Foo['bar']", + }, "type": Object { "detail": undefined, "summary": "Foo['bar']", diff --git a/addons/docs/src/frameworks/react/__testfixtures__/8428-js-static-prop-types/docgen.snapshot b/addons/docs/src/frameworks/react/__testfixtures__/8428-js-static-prop-types/docgen.snapshot index 5f7790bfe512..10a2a46836d4 100644 --- a/addons/docs/src/frameworks/react/__testfixtures__/8428-js-static-prop-types/docgen.snapshot +++ b/addons/docs/src/frameworks/react/__testfixtures__/8428-js-static-prop-types/docgen.snapshot @@ -3,9 +3,6 @@ exports[`react component properties 8428-js-static-prop-types 1`] = ` "function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } -/* eslint-disable react/no-unused-prop-types */ - -/* eslint-disable react/require-default-props */ import React from 'react'; import PropTypes from 'prop-types'; // eslint-disable-next-line react/prefer-stateless-function diff --git a/addons/docs/src/frameworks/react/__testfixtures__/8428-js-static-prop-types/input.js b/addons/docs/src/frameworks/react/__testfixtures__/8428-js-static-prop-types/input.js index 4b416fe973a4..20094fa268dd 100644 --- a/addons/docs/src/frameworks/react/__testfixtures__/8428-js-static-prop-types/input.js +++ b/addons/docs/src/frameworks/react/__testfixtures__/8428-js-static-prop-types/input.js @@ -1,5 +1,3 @@ -/* eslint-disable react/no-unused-prop-types */ -/* eslint-disable react/require-default-props */ import React from 'react'; import PropTypes from 'prop-types'; diff --git a/addons/docs/src/frameworks/react/__testfixtures__/8428-js-static-prop-types/properties.snapshot b/addons/docs/src/frameworks/react/__testfixtures__/8428-js-static-prop-types/properties.snapshot index dd23348be458..343ea8913700 100644 --- a/addons/docs/src/frameworks/react/__testfixtures__/8428-js-static-prop-types/properties.snapshot +++ b/addons/docs/src/frameworks/react/__testfixtures__/8428-js-static-prop-types/properties.snapshot @@ -8,6 +8,9 @@ Object { "description": "Please work...", "name": "test", "required": false, + "sbType": Object { + "name": "string", + }, "type": Object { "detail": undefined, "summary": "string", diff --git a/addons/docs/src/frameworks/react/__testfixtures__/8663-js-styled-components/docgen.snapshot b/addons/docs/src/frameworks/react/__testfixtures__/8663-js-styled-components/docgen.snapshot index eaa94221e220..bf03a19e7769 100644 --- a/addons/docs/src/frameworks/react/__testfixtures__/8663-js-styled-components/docgen.snapshot +++ b/addons/docs/src/frameworks/react/__testfixtures__/8663-js-styled-components/docgen.snapshot @@ -12,7 +12,6 @@ Box.propTypes = { }; export const MyBox = props => /*#__PURE__*/React.createElement(Box, props); MyBox.propTypes = { - // eslint-disable-next-line react/require-default-props bg: PropTypes.string }; export const component = MyBox; diff --git a/addons/docs/src/frameworks/react/__testfixtures__/8663-js-styled-components/input.js b/addons/docs/src/frameworks/react/__testfixtures__/8663-js-styled-components/input.js index 543f9c890606..7eb40f01b946 100644 --- a/addons/docs/src/frameworks/react/__testfixtures__/8663-js-styled-components/input.js +++ b/addons/docs/src/frameworks/react/__testfixtures__/8663-js-styled-components/input.js @@ -13,7 +13,6 @@ Box.propTypes = { export const MyBox = (props) => ; MyBox.propTypes = { - // eslint-disable-next-line react/require-default-props bg: PropTypes.string, }; diff --git a/addons/docs/src/frameworks/react/__testfixtures__/8663-js-styled-components/properties.snapshot b/addons/docs/src/frameworks/react/__testfixtures__/8663-js-styled-components/properties.snapshot index 0681e3864563..e6923faff496 100644 --- a/addons/docs/src/frameworks/react/__testfixtures__/8663-js-styled-components/properties.snapshot +++ b/addons/docs/src/frameworks/react/__testfixtures__/8663-js-styled-components/properties.snapshot @@ -8,6 +8,9 @@ Object { "description": "", "name": "bg", "required": false, + "sbType": Object { + "name": "string", + }, "type": Object { "detail": undefined, "summary": "string", diff --git a/addons/docs/src/frameworks/react/__testfixtures__/9023-js-hoc/docgen.snapshot b/addons/docs/src/frameworks/react/__testfixtures__/9023-js-hoc/docgen.snapshot index 30949868867f..6755a05a5cfc 100644 --- a/addons/docs/src/frameworks/react/__testfixtures__/9023-js-hoc/docgen.snapshot +++ b/addons/docs/src/frameworks/react/__testfixtures__/9023-js-hoc/docgen.snapshot @@ -1,13 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`react component properties 9023-js-hoc 1`] = ` -"/* eslint-disable react/forbid-prop-types */ - -/* eslint-disable react/require-default-props */ - -/* eslint-disable react/no-unused-prop-types */ - -/* eslint-disable react/prefer-stateless-function */ +"/* eslint-disable react/prefer-stateless-function */ import React from 'react'; import PropTypes from 'prop-types'; diff --git a/addons/docs/src/frameworks/react/__testfixtures__/9023-js-hoc/input.js b/addons/docs/src/frameworks/react/__testfixtures__/9023-js-hoc/input.js index 54930c0d3398..3c40e3472a01 100644 --- a/addons/docs/src/frameworks/react/__testfixtures__/9023-js-hoc/input.js +++ b/addons/docs/src/frameworks/react/__testfixtures__/9023-js-hoc/input.js @@ -1,6 +1,3 @@ -/* eslint-disable react/forbid-prop-types */ -/* eslint-disable react/require-default-props */ -/* eslint-disable react/no-unused-prop-types */ /* eslint-disable react/prefer-stateless-function */ import React from 'react'; import PropTypes from 'prop-types'; diff --git a/addons/docs/src/frameworks/react/__testfixtures__/9023-js-hoc/properties.snapshot b/addons/docs/src/frameworks/react/__testfixtures__/9023-js-hoc/properties.snapshot index 6c8e78ce3878..7d6c8e0a2749 100644 --- a/addons/docs/src/frameworks/react/__testfixtures__/9023-js-hoc/properties.snapshot +++ b/addons/docs/src/frameworks/react/__testfixtures__/9023-js-hoc/properties.snapshot @@ -11,6 +11,9 @@ Object { "description": "", "name": "variant", "required": false, + "sbType": Object { + "name": "string", + }, "type": Object { "detail": undefined, "summary": "string", @@ -24,6 +27,9 @@ Object { "description": "", "name": "dismissible", "required": false, + "sbType": Object { + "name": "boolean", + }, "type": Object { "detail": undefined, "summary": "bool", @@ -34,6 +40,10 @@ Object { "description": "", "name": "icon", "required": false, + "sbType": Object { + "name": "other", + "value": "elementType", + }, "type": Object { "detail": undefined, "summary": "elementType", @@ -44,6 +54,9 @@ Object { "description": "", "name": "classes", "required": true, + "sbType": Object { + "name": "object", + }, "type": Object { "detail": undefined, "summary": "object", diff --git a/addons/docs/src/frameworks/react/__testfixtures__/9399-js-proptypes-shape/properties.snapshot b/addons/docs/src/frameworks/react/__testfixtures__/9399-js-proptypes-shape/properties.snapshot index 193a0a5d8423..e831f52c1ab1 100644 --- a/addons/docs/src/frameworks/react/__testfixtures__/9399-js-proptypes-shape/properties.snapshot +++ b/addons/docs/src/frameworks/react/__testfixtures__/9399-js-proptypes-shape/properties.snapshot @@ -8,6 +8,23 @@ Object { "description": "", "name": "areas", "required": true, + "sbType": Object { + "name": "array", + "value": Object { + "name": "object", + "value": Object { + "names": Object { + "name": "array", + "value": Object { + "name": "string", + }, + }, + "position": Object { + "name": "string", + }, + }, + }, + }, "type": Object { "detail": "[object]", "summary": "object[]", diff --git a/addons/docs/src/frameworks/react/__testfixtures__/9465-ts-type-props/properties.snapshot b/addons/docs/src/frameworks/react/__testfixtures__/9465-ts-type-props/properties.snapshot index 67f69b4def7c..e2d4df1dcc0e 100644 --- a/addons/docs/src/frameworks/react/__testfixtures__/9465-ts-type-props/properties.snapshot +++ b/addons/docs/src/frameworks/react/__testfixtures__/9465-ts-type-props/properties.snapshot @@ -11,6 +11,9 @@ Object { "description": "", "name": "disabled", "required": false, + "sbType": Object { + "name": "boolean", + }, "type": Object { "detail": undefined, "summary": "boolean", diff --git a/addons/docs/src/frameworks/react/__testfixtures__/9493-ts-display-name/properties.snapshot b/addons/docs/src/frameworks/react/__testfixtures__/9493-ts-display-name/properties.snapshot index 03d15c0bcb15..f6be2f921ecf 100644 --- a/addons/docs/src/frameworks/react/__testfixtures__/9493-ts-display-name/properties.snapshot +++ b/addons/docs/src/frameworks/react/__testfixtures__/9493-ts-display-name/properties.snapshot @@ -11,6 +11,24 @@ Object { "description": "A title that brings attention to the alert.", "name": "title", "required": false, + "sbType": Object { + "name": "union", + "raw": "'Code Red' | 'Code Yellow' | 'Code Green'", + "value": Array [ + Object { + "name": "other", + "value": "literal", + }, + Object { + "name": "other", + "value": "literal", + }, + Object { + "name": "other", + "value": "literal", + }, + ], + }, "type": Object { "detail": undefined, "summary": "union", @@ -21,6 +39,9 @@ Object { "description": "A message alerting about Empire activities.", "name": "message", "required": true, + "sbType": Object { + "name": "string", + }, "type": Object { "detail": undefined, "summary": "string", diff --git a/addons/docs/src/frameworks/react/__testfixtures__/9556-ts-react-default-exports/properties.snapshot b/addons/docs/src/frameworks/react/__testfixtures__/9556-ts-react-default-exports/properties.snapshot index 036e96d9ee27..daa979f95c81 100644 --- a/addons/docs/src/frameworks/react/__testfixtures__/9556-ts-react-default-exports/properties.snapshot +++ b/addons/docs/src/frameworks/react/__testfixtures__/9556-ts-react-default-exports/properties.snapshot @@ -11,6 +11,9 @@ Object { "description": "", "name": "isDisabled", "required": false, + "sbType": Object { + "name": "boolean", + }, "type": Object { "detail": undefined, "summary": "boolean", diff --git a/addons/docs/src/frameworks/react/__testfixtures__/9575-ts-camel-case/docgen.snapshot b/addons/docs/src/frameworks/react/__testfixtures__/9575-ts-camel-case/docgen.snapshot index cf69c92d8f84..9494b9c88cd9 100644 --- a/addons/docs/src/frameworks/react/__testfixtures__/9575-ts-camel-case/docgen.snapshot +++ b/addons/docs/src/frameworks/react/__testfixtures__/9575-ts-camel-case/docgen.snapshot @@ -11,7 +11,6 @@ const iconButton = function IconButton(props) { }; iconButton.propTypes = { - // eslint-disable-next-line react/no-unused-prop-types color: PropTypes.string }; iconButton.defaultProps = { diff --git a/addons/docs/src/frameworks/react/__testfixtures__/9575-ts-camel-case/input.tsx b/addons/docs/src/frameworks/react/__testfixtures__/9575-ts-camel-case/input.tsx index 2a146297e856..20ddf073f781 100644 --- a/addons/docs/src/frameworks/react/__testfixtures__/9575-ts-camel-case/input.tsx +++ b/addons/docs/src/frameworks/react/__testfixtures__/9575-ts-camel-case/input.tsx @@ -13,7 +13,6 @@ const iconButton: FC = function IconButton(props) { }; iconButton.propTypes = { - // eslint-disable-next-line react/no-unused-prop-types color: PropTypes.string, }; diff --git a/addons/docs/src/frameworks/react/__testfixtures__/9575-ts-camel-case/properties.snapshot b/addons/docs/src/frameworks/react/__testfixtures__/9575-ts-camel-case/properties.snapshot index 091cab030b64..4ca7be31120d 100644 --- a/addons/docs/src/frameworks/react/__testfixtures__/9575-ts-camel-case/properties.snapshot +++ b/addons/docs/src/frameworks/react/__testfixtures__/9575-ts-camel-case/properties.snapshot @@ -11,6 +11,9 @@ Object { "description": "", "name": "color", "required": false, + "sbType": Object { + "name": "string", + }, "type": Object { "detail": undefined, "summary": "string", diff --git a/addons/docs/src/frameworks/react/__testfixtures__/9591-ts-import-types/properties.snapshot b/addons/docs/src/frameworks/react/__testfixtures__/9591-ts-import-types/properties.snapshot index 73cf8eb0605a..cc8d448239d6 100644 --- a/addons/docs/src/frameworks/react/__testfixtures__/9591-ts-import-types/properties.snapshot +++ b/addons/docs/src/frameworks/react/__testfixtures__/9591-ts-import-types/properties.snapshot @@ -8,6 +8,9 @@ Object { "description": "", "name": "other", "required": false, + "sbType": Object { + "name": "number", + }, "type": Object { "detail": undefined, "summary": "number", diff --git a/addons/docs/src/frameworks/react/__testfixtures__/9592-ts-styled-props/properties.snapshot b/addons/docs/src/frameworks/react/__testfixtures__/9592-ts-styled-props/properties.snapshot index d776bdfb204b..3298e7c6d942 100644 --- a/addons/docs/src/frameworks/react/__testfixtures__/9592-ts-styled-props/properties.snapshot +++ b/addons/docs/src/frameworks/react/__testfixtures__/9592-ts-styled-props/properties.snapshot @@ -8,6 +8,9 @@ Object { "description": "", "name": "title", "required": true, + "sbType": Object { + "name": "string", + }, "type": Object { "detail": undefined, "summary": "string", diff --git a/addons/docs/src/frameworks/react/__testfixtures__/9668-js-proptypes-no-jsdoc/docgen.snapshot b/addons/docs/src/frameworks/react/__testfixtures__/9668-js-proptypes-no-jsdoc/docgen.snapshot index 2febe16f77f4..1d189717438e 100644 --- a/addons/docs/src/frameworks/react/__testfixtures__/9668-js-proptypes-no-jsdoc/docgen.snapshot +++ b/addons/docs/src/frameworks/react/__testfixtures__/9668-js-proptypes-no-jsdoc/docgen.snapshot @@ -1,12 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`react component properties 9668-js-proptypes-no-jsdoc 1`] = ` -"/* eslint-disable react/require-default-props */ - -/* eslint-disable react/no-unused-prop-types */ - -/* eslint-disable react/forbid-prop-types */ -import React from 'react'; +"import React from 'react'; import PropTypes from 'prop-types'; const CCTable = props => /*#__PURE__*/React.createElement(React.Fragment, null, JSON.stringify(props)); diff --git a/addons/docs/src/frameworks/react/__testfixtures__/9668-js-proptypes-no-jsdoc/input.js b/addons/docs/src/frameworks/react/__testfixtures__/9668-js-proptypes-no-jsdoc/input.js index 209fb248c5d4..2e66bdd696d2 100644 --- a/addons/docs/src/frameworks/react/__testfixtures__/9668-js-proptypes-no-jsdoc/input.js +++ b/addons/docs/src/frameworks/react/__testfixtures__/9668-js-proptypes-no-jsdoc/input.js @@ -1,6 +1,3 @@ -/* eslint-disable react/require-default-props */ -/* eslint-disable react/no-unused-prop-types */ -/* eslint-disable react/forbid-prop-types */ import React from 'react'; import PropTypes from 'prop-types'; diff --git a/addons/docs/src/frameworks/react/__testfixtures__/9668-js-proptypes-no-jsdoc/properties.snapshot b/addons/docs/src/frameworks/react/__testfixtures__/9668-js-proptypes-no-jsdoc/properties.snapshot index 4dea4decbacb..3c8760699b96 100644 --- a/addons/docs/src/frameworks/react/__testfixtures__/9668-js-proptypes-no-jsdoc/properties.snapshot +++ b/addons/docs/src/frameworks/react/__testfixtures__/9668-js-proptypes-no-jsdoc/properties.snapshot @@ -8,6 +8,10 @@ Object { "description": "", "name": "heads", "required": true, + "sbType": Object { + "name": "array", + "value": undefined, + }, "type": Object { "detail": undefined, "summary": "array", @@ -18,6 +22,9 @@ Object { "description": "", "name": "onAddClick", "required": false, + "sbType": Object { + "name": "function", + }, "type": Object { "detail": undefined, "summary": "func", diff --git a/addons/docs/src/frameworks/react/__testfixtures__/9721-ts-deprecated-jsdoc/properties.snapshot b/addons/docs/src/frameworks/react/__testfixtures__/9721-ts-deprecated-jsdoc/properties.snapshot index f2790bd2dcea..db5588270f7c 100644 --- a/addons/docs/src/frameworks/react/__testfixtures__/9721-ts-deprecated-jsdoc/properties.snapshot +++ b/addons/docs/src/frameworks/react/__testfixtures__/9721-ts-deprecated-jsdoc/properties.snapshot @@ -8,6 +8,9 @@ Object { "description": "", "name": "width", "required": true, + "sbType": Object { + "name": "number", + }, "type": Object { "detail": undefined, "summary": "number", @@ -18,6 +21,18 @@ Object { "description": "The size (replaces width)", "name": "size", "required": true, + "sbType": Object { + "name": "object", + "raw": "{ width: number; height: number }", + "value": Object { + "height": Object { + "name": "number", + }, + "width": Object { + "name": "number", + }, + }, + }, "type": Object { "detail": undefined, "summary": "signature", diff --git a/addons/docs/src/frameworks/react/__testfixtures__/9764-ts-extend-props/properties.snapshot b/addons/docs/src/frameworks/react/__testfixtures__/9764-ts-extend-props/properties.snapshot index caf90d5ad270..3f3b110e6ddb 100644 --- a/addons/docs/src/frameworks/react/__testfixtures__/9764-ts-extend-props/properties.snapshot +++ b/addons/docs/src/frameworks/react/__testfixtures__/9764-ts-extend-props/properties.snapshot @@ -8,6 +8,18 @@ Object { "description": "The input content value", "name": "value", "required": false, + "sbType": Object { + "name": "union", + "raw": "string | number", + "value": Array [ + Object { + "name": "string", + }, + Object { + "name": "number", + }, + ], + }, "type": Object { "detail": undefined, "summary": "union", @@ -18,6 +30,9 @@ Object { "description": "", "name": "defaultChecked", "required": false, + "sbType": Object { + "name": "boolean", + }, "type": Object { "detail": undefined, "summary": "boolean", @@ -28,6 +43,9 @@ Object { "description": "", "name": "checked", "required": false, + "sbType": Object { + "name": "boolean", + }, "type": Object { "detail": undefined, "summary": "boolean", diff --git a/addons/docs/src/frameworks/react/__testfixtures__/9827-ts-default-values/properties.snapshot b/addons/docs/src/frameworks/react/__testfixtures__/9827-ts-default-values/properties.snapshot index 4b0a64e8e37c..5d944694e62b 100644 --- a/addons/docs/src/frameworks/react/__testfixtures__/9827-ts-default-values/properties.snapshot +++ b/addons/docs/src/frameworks/react/__testfixtures__/9827-ts-default-values/properties.snapshot @@ -11,6 +11,9 @@ Object { "description": "", "name": "title", "required": false, + "sbType": Object { + "name": "string", + }, "type": Object { "detail": undefined, "summary": "string", @@ -21,6 +24,9 @@ Object { "description": "", "name": "foo", "required": true, + "sbType": Object { + "name": "boolean", + }, "type": Object { "detail": undefined, "summary": "boolean", @@ -31,6 +37,15 @@ Object { "description": "", "name": "bar", "required": false, + "sbType": Object { + "name": "array", + "raw": "string[]", + "value": Array [ + Object { + "name": "string", + }, + ], + }, "type": Object { "detail": undefined, "summary": "Array", diff --git a/addons/docs/src/frameworks/react/__testfixtures__/9922-ts-component-props/properties.snapshot b/addons/docs/src/frameworks/react/__testfixtures__/9922-ts-component-props/properties.snapshot index f59e769ff676..d9342f6004e2 100644 --- a/addons/docs/src/frameworks/react/__testfixtures__/9922-ts-component-props/properties.snapshot +++ b/addons/docs/src/frameworks/react/__testfixtures__/9922-ts-component-props/properties.snapshot @@ -8,6 +8,9 @@ Object { "description": "", "name": "spacing", "required": true, + "sbType": Object { + "name": "number", + }, "type": Object { "detail": undefined, "summary": "number", diff --git a/addons/docs/src/frameworks/react/__testfixtures__/js-class-component/properties.snapshot b/addons/docs/src/frameworks/react/__testfixtures__/js-class-component/properties.snapshot index 62dea7473922..abceadfadeaf 100644 --- a/addons/docs/src/frameworks/react/__testfixtures__/js-class-component/properties.snapshot +++ b/addons/docs/src/frameworks/react/__testfixtures__/js-class-component/properties.snapshot @@ -8,6 +8,10 @@ Object { "description": "PropTypes description", "name": "children", "required": true, + "sbType": Object { + "name": "other", + "value": "node", + }, "type": Object { "detail": undefined, "summary": "node", diff --git a/addons/docs/src/frameworks/react/__testfixtures__/ts-function-component/properties.snapshot b/addons/docs/src/frameworks/react/__testfixtures__/ts-function-component/properties.snapshot index 03c2ae1025ba..85ea3277565e 100644 --- a/addons/docs/src/frameworks/react/__testfixtures__/ts-function-component/properties.snapshot +++ b/addons/docs/src/frameworks/react/__testfixtures__/ts-function-component/properties.snapshot @@ -11,6 +11,9 @@ Object { "description": "Is primary?", "name": "primary", "required": false, + "sbType": Object { + "name": "boolean", + }, "type": Object { "detail": undefined, "summary": "boolean", @@ -24,6 +27,9 @@ Object { "description": "default is false", "name": "secondary", "required": false, + "sbType": Object { + "name": "boolean", + }, "type": Object { "detail": undefined, "summary": "boolean", @@ -34,6 +40,10 @@ Object { "description": "Simple click handler", "name": "onClick", "required": false, + "sbType": Object { + "name": "function", + "raw": "() => void", + }, "type": Object { "detail": undefined, "summary": "signature", diff --git a/addons/docs/src/frameworks/react/config.ts b/addons/docs/src/frameworks/react/config.ts index aa9c450b2b1f..a32d4b3d26c3 100644 --- a/addons/docs/src/frameworks/react/config.ts +++ b/addons/docs/src/frameworks/react/config.ts @@ -1,14 +1,13 @@ -import { addParameters } from '@storybook/client-api'; import { StoryFn } from '@storybook/addons'; -import { extractProps } from './extractProps'; +import { extractArgTypes } from './extractArgTypes'; import { extractComponentDescription } from '../../lib/docgen'; -addParameters({ +export const parameters = { docs: { // react is Storybook's "native" framework, so it's stories are inherently prepared to be rendered inline // NOTE: that the result is a react element. Hooks support is provided by the outer code. prepareForInline: (storyFn: StoryFn) => storyFn(), - extractProps, + extractArgTypes, extractComponentDescription, }, -}); +}; diff --git a/addons/docs/src/frameworks/react/extractArgTypes.ts b/addons/docs/src/frameworks/react/extractArgTypes.ts new file mode 100644 index 000000000000..ee8e739b0953 --- /dev/null +++ b/addons/docs/src/frameworks/react/extractArgTypes.ts @@ -0,0 +1,29 @@ +import { PropDef, PropsTableRowsProps } from '@storybook/components'; +import { ArgTypes } from '@storybook/api'; +import { ArgTypesExtractor } from '../../lib/docgen'; +import { extractProps } from './extractProps'; + +export const extractArgTypes: ArgTypesExtractor = (component) => { + if (component) { + const props = extractProps(component); + const { rows } = props as PropsTableRowsProps; + if (rows) { + return rows.reduce((acc: ArgTypes, row: PropDef) => { + const { type, sbType, defaultValue, jsDocTags } = row; + acc[row.name] = { + ...row, + defaultValue: defaultValue && (defaultValue.detail || defaultValue.summary), + type: sbType, + table: { + type, + jsDocTags, + defaultValue, + }, + }; + return acc; + }, {}); + } + } + + return null; +}; diff --git a/addons/docs/src/frameworks/react/react-argtypes.stories.tsx b/addons/docs/src/frameworks/react/react-argtypes.stories.tsx new file mode 100644 index 000000000000..8b04700d4282 --- /dev/null +++ b/addons/docs/src/frameworks/react/react-argtypes.stories.tsx @@ -0,0 +1,108 @@ +import React, { useState } from 'react'; +import mapValues from 'lodash/mapValues'; +import { storiesOf } from '@storybook/react'; +import { ArgsTable } from '@storybook/components'; +import { action } from '@storybook/addon-actions'; +import { Args } from '@storybook/api'; +import { combineParameters } from '@storybook/client-api'; + +import { extractArgTypes } from './extractArgTypes'; +import { inferControls } from '../common/inferControls'; +import { Component } from '../../blocks'; + +const argsTableProps = (component: Component) => { + const argTypes = extractArgTypes(component); + const controls = inferControls(argTypes); + const rows = combineParameters(argTypes, controls); + return { rows }; +}; + +const ArgsStory = ({ component }: any) => { + const { rows } = argsTableProps(component); + const initialArgs = mapValues(rows, () => null) as Args; + + const [args, setArgs] = useState(initialArgs); + return ( + <> + + + + + + {Object.entries(args).map(([key, val]) => ( + + + + + ))} +
keyval
{key}{JSON.stringify(val, null, 2)}
+ setArgs({ ...args, ...val })} /> + + ); +}; + +const typescriptFixtures = [ + 'aliases', + 'arrays', + 'enums', + 'functions', + 'interfaces', + 'intersections', + 'records', + 'scalars', + 'tuples', + 'unions', +]; + +const typescriptStories = storiesOf('ArgTypes/TypeScript', module); +typescriptFixtures.forEach((fixture) => { + // eslint-disable-next-line import/no-dynamic-require, global-require, no-shadow + const { Component } = require(`../../lib/sbtypes/__testfixtures__/typescript/${fixture}`); + typescriptStories.add(fixture, () => ); +}); + +const proptypesFixtures = ['arrays', 'enums', 'misc', 'objects', 'react', 'scalars']; + +const proptypesStories = storiesOf('ArgTypes/PropTypes', module); +proptypesFixtures.forEach((fixture) => { + // eslint-disable-next-line import/no-dynamic-require, global-require, no-shadow + const { Component } = require(`../../lib/sbtypes/__testfixtures__/proptypes/${fixture}`); + proptypesStories.add(fixture, () => ); +}); + +const issuesFixtures = [ + 'js-class-component', + 'ts-function-component', + '9399-js-proptypes-shape', + '8663-js-styled-components', + '9626-js-default-values', + '9668-js-proptypes-no-jsdoc', + '8143-ts-react-fc-generics', + '8143-ts-imported-types', + '8279-js-styled-docgen', + '8140-js-prop-types-oneof', + '9023-js-hoc', + '8740-ts-multi-props', + '9556-ts-react-default-exports', + '9592-ts-styled-props', + '9591-ts-import-types', + '9721-ts-deprecated-jsdoc', + '9827-ts-default-values', + '9586-js-react-memo', + '9575-ts-camel-case', + '9493-ts-display-name', + '8894-9511-ts-forward-ref', + '9465-ts-type-props', + '8428-js-static-prop-types', + '9764-ts-extend-props', + '9922-ts-component-props', +]; + +const issuesStories = storiesOf('ArgTypes/Issues', module); +issuesFixtures.forEach((fixture) => { + // eslint-disable-next-line import/no-dynamic-require, global-require + const { component } = require(`./__testfixtures__/${fixture}/input`); + const props = argsTableProps(component); + + issuesStories.add(fixture, () => ); +}); diff --git a/addons/docs/src/frameworks/react/react-properties.stories.tsx b/addons/docs/src/frameworks/react/react-properties.stories.tsx deleted file mode 100644 index 287d4cf35cf6..000000000000 --- a/addons/docs/src/frameworks/react/react-properties.stories.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import React from 'react'; -import { storiesOf } from '@storybook/react'; -import { PropsTable } from '@storybook/components'; -import { extractProps } from './extractProps'; - -const fixtures = [ - 'js-class-component', - 'ts-function-component', - '9399-js-proptypes-shape', - '8663-js-styled-components', - '9626-js-default-values', - '9668-js-proptypes-no-jsdoc', - '8143-ts-react-fc-generics', - '8143-ts-imported-types', - '8279-js-styled-docgen', - '8140-js-prop-types-oneof', - '9023-js-hoc', - '8740-ts-multi-props', - '9556-ts-react-default-exports', - '9592-ts-styled-props', - '9591-ts-import-types', - '9721-ts-deprecated-jsdoc', - '9827-ts-default-values', - '9586-js-react-memo', - '9575-ts-camel-case', - '9493-ts-display-name', - '8894-9511-ts-forward-ref', - '9465-ts-type-props', - '8428-js-static-prop-types', - '9764-ts-extend-props', - '9922-ts-component-props', -]; - -const stories = storiesOf('Properties/React', module); - -fixtures.forEach((fixture) => { - // eslint-disable-next-line import/no-dynamic-require, global-require - const { component } = require(`./__testfixtures__/${fixture}/input`); - const props = extractProps(component); - stories.add(fixture, () => ); -}); diff --git a/addons/docs/src/lib/docgen/createPropDef.ts b/addons/docs/src/lib/docgen/createPropDef.ts index a100a188e1e8..2fb88bd66d78 100644 --- a/addons/docs/src/lib/docgen/createPropDef.ts +++ b/addons/docs/src/lib/docgen/createPropDef.ts @@ -5,6 +5,7 @@ import { createSummaryValue } from '../utils'; import { createFlowPropDef } from './flow/createPropDef'; import { isDefaultValueBlacklisted } from './utils/defaultValue'; import { createTsPropDef } from './typeScript/createPropDef'; +import { convert } from '../sbtypes'; export type PropDefFactory = ( propName: string, @@ -72,18 +73,21 @@ function applyJsDocResult(propDef: PropDef, jsDocParsingResult: JsDocParsingResu export const javaScriptFactory: PropDefFactory = (propName, docgenInfo, jsDocParsingResult) => { const propDef = createBasicPropDef(propName, docgenInfo.type, docgenInfo); + propDef.sbType = convert(docgenInfo); return applyJsDocResult(propDef, jsDocParsingResult); }; export const tsFactory: PropDefFactory = (propName, docgenInfo, jsDocParsingResult) => { const propDef = createTsPropDef(propName, docgenInfo); + propDef.sbType = convert(docgenInfo); return applyJsDocResult(propDef, jsDocParsingResult); }; export const flowFactory: PropDefFactory = (propName, docgenInfo, jsDocParsingResult) => { const propDef = createFlowPropDef(propName, docgenInfo); + propDef.sbType = convert(docgenInfo); return applyJsDocResult(propDef, jsDocParsingResult); }; diff --git a/addons/docs/src/lib/docgen/types.ts b/addons/docs/src/lib/docgen/types.ts index cac1ccf3437a..f95a54e84641 100644 --- a/addons/docs/src/lib/docgen/types.ts +++ b/addons/docs/src/lib/docgen/types.ts @@ -1,8 +1,11 @@ import { PropsTableProps } from '@storybook/components'; +import { ArgTypes } from '@storybook/api'; import { Component } from '../../blocks/types'; export type PropsExtractor = (component: Component) => PropsTableProps | null; +export type ArgTypesExtractor = (component: Component) => ArgTypes | null; + export interface DocgenType { name: string; description?: string; diff --git a/addons/docs/src/lib/sbtypes/__testfixtures__/proptypes/arrays.js b/addons/docs/src/lib/sbtypes/__testfixtures__/proptypes/arrays.js new file mode 100644 index 000000000000..0f1ebcdae092 --- /dev/null +++ b/addons/docs/src/lib/sbtypes/__testfixtures__/proptypes/arrays.js @@ -0,0 +1,13 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +export const Component = (props) => <>JSON.stringify(props); +Component.propTypes = { + optionalArray: PropTypes.array, + arrayOfStrings: PropTypes.arrayOf(PropTypes.string), + arrayOfShape: PropTypes.arrayOf( + PropTypes.shape({ + active: PropTypes.bool, + }) + ), +}; diff --git a/addons/docs/src/lib/sbtypes/__testfixtures__/proptypes/enums.js b/addons/docs/src/lib/sbtypes/__testfixtures__/proptypes/enums.js new file mode 100644 index 000000000000..60f51359b6f5 --- /dev/null +++ b/addons/docs/src/lib/sbtypes/__testfixtures__/proptypes/enums.js @@ -0,0 +1,8 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +export const Component = (props) => <>JSON.stringify(props); +Component.propTypes = { + oneOfNumber: PropTypes.oneOf([1, 2, 3]), + oneOfString: PropTypes.oneOf(['static', 'timed']), +}; diff --git a/addons/docs/src/lib/sbtypes/__testfixtures__/proptypes/misc.js b/addons/docs/src/lib/sbtypes/__testfixtures__/proptypes/misc.js new file mode 100644 index 000000000000..e662474d1ab1 --- /dev/null +++ b/addons/docs/src/lib/sbtypes/__testfixtures__/proptypes/misc.js @@ -0,0 +1,15 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +export const Component = (props) => <>JSON.stringify(props); +Component.propTypes = { + // An object that could be one of many types + optionalUnion: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.number, + PropTypes.instanceOf(Object), + ]), + optionalMessage: PropTypes.instanceOf(Object), + // A value of any data type + requiredAny: PropTypes.any.isRequired, +}; diff --git a/addons/docs/src/lib/sbtypes/__testfixtures__/proptypes/objects.js b/addons/docs/src/lib/sbtypes/__testfixtures__/proptypes/objects.js new file mode 100644 index 000000000000..09036cf88088 --- /dev/null +++ b/addons/docs/src/lib/sbtypes/__testfixtures__/proptypes/objects.js @@ -0,0 +1,16 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +export const Component = (props) => <>JSON.stringify(props); +Component.propTypes = { + optionalObject: PropTypes.object, + optionalObjectOf: PropTypes.objectOf(PropTypes.number), + optionalObjectWithShape: PropTypes.shape({ + color: PropTypes.string, + fontSize: PropTypes.number, + }), + optionalObjectWithStrictShape: PropTypes.exact({ + name: PropTypes.string, + quantity: PropTypes.number, + }), +}; diff --git a/addons/docs/src/lib/sbtypes/__testfixtures__/proptypes/react.js b/addons/docs/src/lib/sbtypes/__testfixtures__/proptypes/react.js new file mode 100644 index 000000000000..fd54e9307663 --- /dev/null +++ b/addons/docs/src/lib/sbtypes/__testfixtures__/proptypes/react.js @@ -0,0 +1,13 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +export const Component = (props) => <>JSON.stringify(props); +Component.propTypes = { + // Anything that can be rendered: numbers, strings, elements or an array + // (or fragment) containing these types. + optionalNode: PropTypes.node, + // A React element. + optionalElement: PropTypes.element, + // A React element type (ie. MyComponent). + optionalElementType: PropTypes.elementType, +}; diff --git a/addons/docs/src/lib/sbtypes/__testfixtures__/proptypes/scalars.js b/addons/docs/src/lib/sbtypes/__testfixtures__/proptypes/scalars.js new file mode 100644 index 000000000000..7e74151596b0 --- /dev/null +++ b/addons/docs/src/lib/sbtypes/__testfixtures__/proptypes/scalars.js @@ -0,0 +1,11 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +export const Component = (props) => <>JSON.stringify(props); +Component.propTypes = { + optionalBool: PropTypes.bool, + optionalFunc: PropTypes.func, + optionalNumber: PropTypes.number, + optionalString: PropTypes.string, + optionalSymbol: PropTypes.symbol, +}; diff --git a/addons/docs/src/lib/sbtypes/__testfixtures__/typescript/aliases.tsx b/addons/docs/src/lib/sbtypes/__testfixtures__/typescript/aliases.tsx new file mode 100644 index 000000000000..528ffec6ba25 --- /dev/null +++ b/addons/docs/src/lib/sbtypes/__testfixtures__/typescript/aliases.tsx @@ -0,0 +1,14 @@ +import React, { FC } from 'react'; + +type StringAlias = string; +type NumberAlias = number; +type AliasesIntersection = StringAlias & NumberAlias; +type AliasesUnion = StringAlias | NumberAlias; +type GenericAlias = { value: T }; +interface Props { + typeAlias: StringAlias; + aliasesIntersection: AliasesIntersection; + aliasesUnion: AliasesUnion; + genericAlias: GenericAlias; +} +export const Component: FC = (props: Props) => <>JSON.stringify(props); diff --git a/addons/docs/src/lib/sbtypes/__testfixtures__/typescript/arrays.tsx b/addons/docs/src/lib/sbtypes/__testfixtures__/typescript/arrays.tsx new file mode 100644 index 000000000000..74c8a4d56a07 --- /dev/null +++ b/addons/docs/src/lib/sbtypes/__testfixtures__/typescript/arrays.tsx @@ -0,0 +1,17 @@ +import React, { FC } from 'react'; + +interface ItemInterface { + text: string; + value: string; +} +interface Point { + x: number; + y: number; +} +interface Props { + arrayOfPoints: Point[]; + arrayOfInlineObjects: { w: number; h: number }[]; + arrayOfPrimitive: string[]; + arrayOfComplexObject: ItemInterface[]; +} +export const Component: FC = (props: Props) => <>JSON.stringify(props); diff --git a/addons/docs/src/lib/sbtypes/__testfixtures__/typescript/enums.tsx b/addons/docs/src/lib/sbtypes/__testfixtures__/typescript/enums.tsx new file mode 100644 index 000000000000..9cf04170aab9 --- /dev/null +++ b/addons/docs/src/lib/sbtypes/__testfixtures__/typescript/enums.tsx @@ -0,0 +1,23 @@ +import React, { FC } from 'react'; + +enum DefaultEnum { + TopLeft, + TopRight, + TopCenter, +} +enum NumericEnum { + TopLeft = 0, + TopRight, + TopCenter, +} +enum StringEnum { + TopLeft = 'top-left', + TopRight = 'top-right', + TopCenter = 'top-center', +} +interface Props { + defaultEnum: DefaultEnum; + numericEnum: NumericEnum; + stringEnum: StringEnum; +} +export const Component: FC = (props: Props) => <>JSON.stringify(props); diff --git a/addons/docs/src/lib/sbtypes/__testfixtures__/typescript/functions.tsx b/addons/docs/src/lib/sbtypes/__testfixtures__/typescript/functions.tsx new file mode 100644 index 000000000000..cbe35a3ac0fc --- /dev/null +++ b/addons/docs/src/lib/sbtypes/__testfixtures__/typescript/functions.tsx @@ -0,0 +1,14 @@ +import React, { FC } from 'react'; + +interface ItemInterface { + text: string; + value: string; +} +interface Props { + onClick?: () => void; + voidFunc: () => void; + funcWithArgsAndReturns: (a: string, b: string) => string; + funcWithUnionArg: (a: string | number) => string; + funcWithMultipleUnionReturns: () => string | ItemInterface; +} +export const Component: FC = (props: Props) => <>JSON.stringify(props); diff --git a/addons/docs/src/lib/sbtypes/__testfixtures__/typescript/interfaces.tsx b/addons/docs/src/lib/sbtypes/__testfixtures__/typescript/interfaces.tsx new file mode 100644 index 000000000000..3f931b09c087 --- /dev/null +++ b/addons/docs/src/lib/sbtypes/__testfixtures__/typescript/interfaces.tsx @@ -0,0 +1,14 @@ +import React, { FC } from 'react'; + +interface ItemInterface { + text: string; + value: string; +} +interface GenericInterface { + value: T; +} +interface Props { + interface: ItemInterface; + genericInterface: GenericInterface; +} +export const Component: FC = (props: Props) => <>JSON.stringify(props); diff --git a/addons/docs/src/lib/sbtypes/__testfixtures__/typescript/intersections.tsx b/addons/docs/src/lib/sbtypes/__testfixtures__/typescript/intersections.tsx new file mode 100644 index 000000000000..a075158cd580 --- /dev/null +++ b/addons/docs/src/lib/sbtypes/__testfixtures__/typescript/intersections.tsx @@ -0,0 +1,15 @@ +import React, { FC } from 'react'; + +interface ItemInterface { + text: string; + value: string; +} +interface PersonInterface { + name: string; +} +type InterfaceIntersection = ItemInterface & PersonInterface; +interface Props { + intersectionType: InterfaceIntersection; + intersectionWithInlineType: ItemInterface & { inlineValue: string }; +} +export const Component: FC = (props: Props) => <>JSON.stringify(props); diff --git a/addons/docs/src/lib/sbtypes/__testfixtures__/typescript/records.tsx b/addons/docs/src/lib/sbtypes/__testfixtures__/typescript/records.tsx new file mode 100644 index 000000000000..b8e541f35aba --- /dev/null +++ b/addons/docs/src/lib/sbtypes/__testfixtures__/typescript/records.tsx @@ -0,0 +1,11 @@ +import React, { FC } from 'react'; + +interface ItemInterface { + text: string; + value: string; +} +interface Props { + recordOfPrimitive: Record; + recordOfComplexObject: Record; +} +export const Component: FC = (props: Props) => <>JSON.stringify(props); diff --git a/addons/docs/src/lib/sbtypes/__testfixtures__/typescript/scalars.tsx b/addons/docs/src/lib/sbtypes/__testfixtures__/typescript/scalars.tsx new file mode 100644 index 000000000000..44f533a01b49 --- /dev/null +++ b/addons/docs/src/lib/sbtypes/__testfixtures__/typescript/scalars.tsx @@ -0,0 +1,11 @@ +import React, { FC } from 'react'; + +interface Props { + any: any; + string: string; + bool: boolean; + number: number; + symbol: symbol; + readonly readonlyPrimitive: string; +} +export const Component: FC = (props: Props) => <>JSON.stringify(props); diff --git a/addons/docs/src/lib/sbtypes/__testfixtures__/typescript/tuples.tsx b/addons/docs/src/lib/sbtypes/__testfixtures__/typescript/tuples.tsx new file mode 100644 index 000000000000..490fbd61aa66 --- /dev/null +++ b/addons/docs/src/lib/sbtypes/__testfixtures__/typescript/tuples.tsx @@ -0,0 +1,11 @@ +import React, { FC } from 'react'; + +interface ItemInterface { + text: string; + value: string; +} +interface Props { + tupleOfPrimitive: [string, number]; + tupleWithComplexType: [string, ItemInterface]; +} +export const Component: FC = (props: Props) => <>JSON.stringify(props); diff --git a/addons/docs/src/lib/sbtypes/__testfixtures__/typescript/unions.tsx b/addons/docs/src/lib/sbtypes/__testfixtures__/typescript/unions.tsx new file mode 100644 index 000000000000..eb8bbfc406ff --- /dev/null +++ b/addons/docs/src/lib/sbtypes/__testfixtures__/typescript/unions.tsx @@ -0,0 +1,20 @@ +import React, { FC } from 'react'; + +type Kind = 'default' | 'action'; +enum DefaultEnum { + TopLeft, + TopRight, + TopCenter, +} +enum NumericEnum { + TopLeft = 0, + TopRight, + TopCenter, +} +type EnumUnion = DefaultEnum | NumericEnum; +interface Props { + kind?: Kind; + inlinedNumericLiteralUnion: 0 | 1; + enumUnion: EnumUnion; +} +export const Component: FC = (props: Props) => <>JSON.stringify(props); diff --git a/addons/docs/src/lib/sbtypes/convert.test.ts b/addons/docs/src/lib/sbtypes/convert.test.ts new file mode 100644 index 000000000000..2c498a94b71b --- /dev/null +++ b/addons/docs/src/lib/sbtypes/convert.test.ts @@ -0,0 +1,808 @@ +import 'jest-specific-snapshot'; +import mapValues from 'lodash/mapValues'; +import { transformSync } from '@babel/core'; +import requireFromString from 'require-from-string'; +import fs from 'fs'; + +import { convert } from './convert'; +import { normalizeNewlines } from '../utils'; + +expect.addSnapshotSerializer({ + print: (val: any) => JSON.stringify(val, null, 2), + test: (val) => typeof val !== 'string', +}); + +describe('storybook type system', () => { + describe('TypeScript', () => { + it('scalars', () => { + const input = readFixture('typescript/functions.tsx'); + expect(input).toMatchInlineSnapshot(` + "import React, { FC } from 'react'; + + interface ItemInterface { + text: string; + value: string; + } + interface Props { + onClick?: () => void; + voidFunc: () => void; + funcWithArgsAndReturns: (a: string, b: string) => string; + funcWithUnionArg: (a: string | number) => string; + funcWithMultipleUnionReturns: () => string | ItemInterface; + } + export const Component: FC = (props: Props) => <>JSON.stringify(props); + " + `); + expect(convertTs(input)).toMatchInlineSnapshot(` + { + "onClick": { + "raw": "() => void", + "name": "function" + }, + "voidFunc": { + "raw": "() => void", + "name": "function" + }, + "funcWithArgsAndReturns": { + "raw": "(a: string, b: string) => string", + "name": "function" + }, + "funcWithUnionArg": { + "raw": "(a: string | number) => string", + "name": "function" + }, + "funcWithMultipleUnionReturns": { + "raw": "() => string | ItemInterface", + "name": "function" + } + } + `); + }); + it('functions', () => { + const input = readFixture('typescript/functions.tsx'); + expect(input).toMatchInlineSnapshot(` + "import React, { FC } from 'react'; + + interface ItemInterface { + text: string; + value: string; + } + interface Props { + onClick?: () => void; + voidFunc: () => void; + funcWithArgsAndReturns: (a: string, b: string) => string; + funcWithUnionArg: (a: string | number) => string; + funcWithMultipleUnionReturns: () => string | ItemInterface; + } + export const Component: FC = (props: Props) => <>JSON.stringify(props); + " + `); + expect(convertTs(input)).toMatchInlineSnapshot(` + { + "onClick": { + "raw": "() => void", + "name": "function" + }, + "voidFunc": { + "raw": "() => void", + "name": "function" + }, + "funcWithArgsAndReturns": { + "raw": "(a: string, b: string) => string", + "name": "function" + }, + "funcWithUnionArg": { + "raw": "(a: string | number) => string", + "name": "function" + }, + "funcWithMultipleUnionReturns": { + "raw": "() => string | ItemInterface", + "name": "function" + } + } + `); + }); + it('enums', () => { + const input = readFixture('typescript/enums.tsx'); + expect(input).toMatchInlineSnapshot(` + "import React, { FC } from 'react'; + + enum DefaultEnum { + TopLeft, + TopRight, + TopCenter, + } + enum NumericEnum { + TopLeft = 0, + TopRight, + TopCenter, + } + enum StringEnum { + TopLeft = 'top-left', + TopRight = 'top-right', + TopCenter = 'top-center', + } + interface Props { + defaultEnum: DefaultEnum; + numericEnum: NumericEnum; + stringEnum: StringEnum; + } + export const Component: FC = (props: Props) => <>JSON.stringify(props); + " + `); + expect(convertTs(input)).toMatchInlineSnapshot(` + { + "defaultEnum": { + "name": "other", + "value": "DefaultEnum" + }, + "numericEnum": { + "name": "other", + "value": "NumericEnum" + }, + "stringEnum": { + "name": "other", + "value": "StringEnum" + } + } + `); + }); + it('unions', () => { + const input = readFixture('typescript/unions.tsx'); + expect(input).toMatchInlineSnapshot(` + "import React, { FC } from 'react'; + + type Kind = 'default' | 'action'; + enum DefaultEnum { + TopLeft, + TopRight, + TopCenter, + } + enum NumericEnum { + TopLeft = 0, + TopRight, + TopCenter, + } + type EnumUnion = DefaultEnum | NumericEnum; + interface Props { + kind?: Kind; + inlinedNumericLiteralUnion: 0 | 1; + enumUnion: EnumUnion; + } + export const Component: FC = (props: Props) => <>JSON.stringify(props); + " + `); + expect(convertTs(input)).toMatchInlineSnapshot(` + { + "kind": { + "raw": "'default' | 'action'", + "name": "union", + "value": [ + { + "name": "other", + "value": "literal" + }, + { + "name": "other", + "value": "literal" + } + ] + }, + "inlinedNumericLiteralUnion": { + "raw": "0 | 1", + "name": "union", + "value": [ + { + "name": "other", + "value": "literal" + }, + { + "name": "other", + "value": "literal" + } + ] + }, + "enumUnion": { + "raw": "DefaultEnum | NumericEnum", + "name": "union", + "value": [ + { + "name": "other", + "value": "DefaultEnum" + }, + { + "name": "other", + "value": "NumericEnum" + } + ] + } + } + `); + }); + it('intersections', () => { + const input = readFixture('typescript/intersections.tsx'); + expect(input).toMatchInlineSnapshot(` + "import React, { FC } from 'react'; + + interface ItemInterface { + text: string; + value: string; + } + interface PersonInterface { + name: string; + } + type InterfaceIntersection = ItemInterface & PersonInterface; + interface Props { + intersectionType: InterfaceIntersection; + intersectionWithInlineType: ItemInterface & { inlineValue: string }; + } + export const Component: FC = (props: Props) => <>JSON.stringify(props); + " + `); + expect(convertTs(input)).toMatchInlineSnapshot(` + { + "intersectionType": { + "raw": "ItemInterface & PersonInterface", + "name": "intersection", + "value": [ + { + "name": "other", + "value": "ItemInterface" + }, + { + "name": "other", + "value": "PersonInterface" + } + ] + }, + "intersectionWithInlineType": { + "raw": "ItemInterface & { inlineValue: string }", + "name": "intersection", + "value": [ + { + "name": "other", + "value": "ItemInterface" + }, + { + "raw": "{ inlineValue: string }", + "name": "object", + "value": { + "inlineValue": { + "name": "string" + } + } + } + ] + } + } + `); + }); + it('arrays', () => { + const input = readFixture('typescript/arrays.tsx'); + expect(input).toMatchInlineSnapshot(` + "import React, { FC } from 'react'; + + interface ItemInterface { + text: string; + value: string; + } + interface Point { + x: number; + y: number; + } + interface Props { + arrayOfPoints: Point[]; + arrayOfInlineObjects: { w: number; h: number }[]; + arrayOfPrimitive: string[]; + arrayOfComplexObject: ItemInterface[]; + } + export const Component: FC = (props: Props) => <>JSON.stringify(props); + " + `); + expect(convertTs(input)).toMatchInlineSnapshot(` + { + "arrayOfPoints": { + "raw": "Point[]", + "name": "array", + "value": [ + { + "name": "other", + "value": "Point" + } + ] + }, + "arrayOfInlineObjects": { + "raw": "{ w: number; h: number }[]", + "name": "array", + "value": [ + { + "raw": "{ w: number; h: number }", + "name": "object", + "value": { + "w": { + "name": "number" + }, + "h": { + "name": "number" + } + } + } + ] + }, + "arrayOfPrimitive": { + "raw": "string[]", + "name": "array", + "value": [ + { + "name": "string" + } + ] + }, + "arrayOfComplexObject": { + "raw": "ItemInterface[]", + "name": "array", + "value": [ + { + "name": "other", + "value": "ItemInterface" + } + ] + } + } + `); + }); + it('interfaces', () => { + const input = readFixture('typescript/interfaces.tsx'); + expect(input).toMatchInlineSnapshot(` + "import React, { FC } from 'react'; + + interface ItemInterface { + text: string; + value: string; + } + interface GenericInterface { + value: T; + } + interface Props { + interface: ItemInterface; + genericInterface: GenericInterface; + } + export const Component: FC = (props: Props) => <>JSON.stringify(props); + " + `); + expect(convertTs(input)).toMatchInlineSnapshot(` + { + "interface": { + "name": "other", + "value": "ItemInterface" + }, + "genericInterface": { + "raw": "GenericInterface", + "name": "other", + "value": "GenericInterface" + } + } + `); + }); + it('records', () => { + const input = readFixture('typescript/records.tsx'); + expect(input).toMatchInlineSnapshot(` + "import React, { FC } from 'react'; + + interface ItemInterface { + text: string; + value: string; + } + interface Props { + recordOfPrimitive: Record; + recordOfComplexObject: Record; + } + export const Component: FC = (props: Props) => <>JSON.stringify(props); + " + `); + expect(convertTs(input)).toMatchInlineSnapshot(` + { + "recordOfPrimitive": { + "raw": "Record", + "name": "other", + "value": "Record" + }, + "recordOfComplexObject": { + "raw": "Record", + "name": "other", + "value": "Record" + } + } + `); + }); + it('aliases', () => { + const input = readFixture('typescript/aliases.tsx'); + expect(input).toMatchInlineSnapshot(` + "import React, { FC } from 'react'; + + type StringAlias = string; + type NumberAlias = number; + type AliasesIntersection = StringAlias & NumberAlias; + type AliasesUnion = StringAlias | NumberAlias; + type GenericAlias = { value: T }; + interface Props { + typeAlias: StringAlias; + aliasesIntersection: AliasesIntersection; + aliasesUnion: AliasesUnion; + genericAlias: GenericAlias; + } + export const Component: FC = (props: Props) => <>JSON.stringify(props); + " + `); + expect(convertTs(input)).toMatchInlineSnapshot(` + { + "typeAlias": { + "name": "string" + }, + "aliasesIntersection": { + "raw": "StringAlias & NumberAlias", + "name": "intersection", + "value": [ + { + "name": "string" + }, + { + "name": "number" + } + ] + }, + "aliasesUnion": { + "raw": "StringAlias | NumberAlias", + "name": "union", + "value": [ + { + "name": "string" + }, + { + "name": "number" + } + ] + }, + "genericAlias": { + "raw": "{ value: T }", + "name": "object", + "value": { + "value": { + "name": "string" + } + } + } + } + `); + }); + it('tuples', () => { + const input = readFixture('typescript/tuples.tsx'); + expect(input).toMatchInlineSnapshot(` + "import React, { FC } from 'react'; + + interface ItemInterface { + text: string; + value: string; + } + interface Props { + tupleOfPrimitive: [string, number]; + tupleWithComplexType: [string, ItemInterface]; + } + export const Component: FC = (props: Props) => <>JSON.stringify(props); + " + `); + expect(convertTs(input)).toMatchInlineSnapshot(` + { + "tupleOfPrimitive": { + "raw": "[string, number]", + "name": "other", + "value": "tuple" + }, + "tupleWithComplexType": { + "raw": "[string, ItemInterface]", + "name": "other", + "value": "tuple" + } + } + `); + }); + }); + describe('PropTypes', () => { + it('scalars', () => { + const input = readFixture('proptypes/scalars.js'); + expect(input).toMatchInlineSnapshot(` + "import React from 'react'; + import PropTypes from 'prop-types'; + + export const Component = (props) => <>JSON.stringify(props); + Component.propTypes = { + optionalBool: PropTypes.bool, + optionalFunc: PropTypes.func, + optionalNumber: PropTypes.number, + optionalString: PropTypes.string, + optionalSymbol: PropTypes.symbol, + }; + " + `); + expect(convertJs(input)).toMatchInlineSnapshot(` + { + "optionalBool": { + "name": "boolean" + }, + "optionalFunc": { + "name": "function" + }, + "optionalNumber": { + "name": "number" + }, + "optionalString": { + "name": "string" + }, + "optionalSymbol": { + "name": "symbol" + } + } + `); + }); + it('arrays', () => { + const input = readFixture('proptypes/arrays.js'); + expect(input).toMatchInlineSnapshot(` + "import React from 'react'; + import PropTypes from 'prop-types'; + + export const Component = (props) => <>JSON.stringify(props); + Component.propTypes = { + optionalArray: PropTypes.array, + arrayOfStrings: PropTypes.arrayOf(PropTypes.string), + arrayOfShape: PropTypes.arrayOf( + PropTypes.shape({ + active: PropTypes.bool, + }) + ), + }; + " + `); + expect(convertJs(input)).toMatchInlineSnapshot(` + { + "optionalArray": { + "name": "array" + }, + "arrayOfStrings": { + "name": "array", + "value": { + "name": "string" + } + }, + "arrayOfShape": { + "name": "array", + "value": { + "name": "object", + "value": { + "active": { + "name": "boolean" + } + } + } + } + } + `); + }); + it('enums', () => { + const input = readFixture('proptypes/enums.js'); + expect(input).toMatchInlineSnapshot(` + "import React from 'react'; + import PropTypes from 'prop-types'; + + export const Component = (props) => <>JSON.stringify(props); + Component.propTypes = { + oneOfNumber: PropTypes.oneOf([1, 2, 3]), + oneOfString: PropTypes.oneOf(['static', 'timed']), + }; + " + `); + expect(convertJs(input)).toMatchInlineSnapshot(` + { + "oneOfNumber": { + "name": "enum", + "value": [ + "1", + "2", + "3" + ] + }, + "oneOfString": { + "name": "enum", + "value": [ + "static", + "timed" + ] + } + } + `); + }); + it('misc', () => { + const input = readFixture('proptypes/misc.js'); + expect(input).toMatchInlineSnapshot(` + "import React from 'react'; + import PropTypes from 'prop-types'; + + export const Component = (props) => <>JSON.stringify(props); + Component.propTypes = { + // An object that could be one of many types + optionalUnion: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.number, + PropTypes.instanceOf(Object), + ]), + optionalMessage: PropTypes.instanceOf(Object), + // A value of any data type + requiredAny: PropTypes.any.isRequired, + }; + " + `); + expect(convertJs(input)).toMatchInlineSnapshot(` + { + "optionalUnion": { + "name": "union", + "value": [ + { + "name": "string" + }, + { + "name": "number" + }, + { + "name": "other", + "value": "instanceOf(Object)" + } + ] + }, + "optionalMessage": { + "name": "other", + "value": "instanceOf(Object)" + }, + "requiredAny": { + "name": "other", + "value": "any" + } + } + `); + }); + it('objects', () => { + const input = readFixture('proptypes/objects.js'); + expect(input).toMatchInlineSnapshot(` + "import React from 'react'; + import PropTypes from 'prop-types'; + + export const Component = (props) => <>JSON.stringify(props); + Component.propTypes = { + optionalObject: PropTypes.object, + optionalObjectOf: PropTypes.objectOf(PropTypes.number), + optionalObjectWithShape: PropTypes.shape({ + color: PropTypes.string, + fontSize: PropTypes.number, + }), + optionalObjectWithStrictShape: PropTypes.exact({ + name: PropTypes.string, + quantity: PropTypes.number, + }), + }; + " + `); + expect(convertJs(input)).toMatchInlineSnapshot(` + { + "optionalObject": { + "name": "object" + }, + "optionalObjectOf": { + "name": "objectOf", + "value": { + "name": "number" + } + }, + "optionalObjectWithShape": { + "name": "object", + "value": { + "color": { + "name": "string" + }, + "fontSize": { + "name": "number" + } + } + }, + "optionalObjectWithStrictShape": { + "name": "object", + "value": { + "name": { + "name": "string" + }, + "quantity": { + "name": "number" + } + } + } + } + `); + }); + it('react', () => { + const input = readFixture('proptypes/react.js'); + expect(input).toMatchInlineSnapshot(` + "import React from 'react'; + import PropTypes from 'prop-types'; + + export const Component = (props) => <>JSON.stringify(props); + Component.propTypes = { + // Anything that can be rendered: numbers, strings, elements or an array + // (or fragment) containing these types. + optionalNode: PropTypes.node, + // A React element. + optionalElement: PropTypes.element, + // A React element type (ie. MyComponent). + optionalElementType: PropTypes.elementType, + }; + " + `); + expect(convertJs(input)).toMatchInlineSnapshot(` + { + "optionalNode": { + "name": "other", + "value": "node" + }, + "optionalElement": { + "name": "other", + "value": "element" + }, + "optionalElementType": { + "name": "other", + "value": "elementType" + } + } + `); + }); + }); +}); + +const readFixture = (fixture: string) => + fs.readFileSync(`${__dirname}/__testfixtures__/${fixture}`).toString(); + +const transformToModule = (inputCode: string) => { + const options = { + presets: [ + [ + '@babel/preset-env', + { + targets: { + esmodules: true, + }, + }, + ], + ], + }; + const { code } = transformSync(inputCode, options); + return normalizeNewlines(code); +}; + +const annotateWithDocgen = (inputCode: string, filename: string) => { + const options = { + presets: ['@babel/typescript', '@babel/react'], + plugins: ['babel-plugin-react-docgen', '@babel/plugin-proposal-class-properties'], + babelrc: false, + filename, + }; + const { code } = transformSync(inputCode, options); + return normalizeNewlines(code); +}; + +const convertCommon = (code: string, fileExt: string) => { + const docgenPretty = annotateWithDocgen(code, `temp.${fileExt}`); + const { Component } = requireFromString(transformToModule(docgenPretty)); + // eslint-disable-next-line no-underscore-dangle + const { props = {} } = Component.__docgenInfo || {}; + const types = mapValues(props, (prop) => convert(prop)); + return types; +}; + +const convertTs = (code: string) => convertCommon(code, 'tsx'); + +const convertJs = (code: string) => convertCommon(code, 'js'); diff --git a/addons/docs/src/lib/sbtypes/convert.ts b/addons/docs/src/lib/sbtypes/convert.ts new file mode 100644 index 000000000000..9b7b0fb4df27 --- /dev/null +++ b/addons/docs/src/lib/sbtypes/convert.ts @@ -0,0 +1,11 @@ +import { DocgenInfo } from '../docgen/types'; +import { convert as tsConvert, TSType } from './typescript'; +import { convert as propTypesConvert } from './proptypes'; + +export const convert = (docgenInfo: DocgenInfo) => { + const { type, tsType } = docgenInfo; + if (type != null) return propTypesConvert(type); + if (tsType != null) return tsConvert(tsType as TSType); + + return null; +}; diff --git a/addons/docs/src/lib/sbtypes/index.ts b/addons/docs/src/lib/sbtypes/index.ts new file mode 100644 index 000000000000..0ef743fdb7ef --- /dev/null +++ b/addons/docs/src/lib/sbtypes/index.ts @@ -0,0 +1,2 @@ +export * from './convert'; +export * from './types'; diff --git a/addons/docs/src/lib/sbtypes/proptypes/convert.ts b/addons/docs/src/lib/sbtypes/proptypes/convert.ts new file mode 100644 index 000000000000..4b2e3c4e3322 --- /dev/null +++ b/addons/docs/src/lib/sbtypes/proptypes/convert.ts @@ -0,0 +1,46 @@ +/* eslint-disable no-case-declarations */ +import mapValues from 'lodash/mapValues'; +import { PTType } from './types'; +import { SBType } from '../types'; + +const QUOTE_REGEX = /^['"]|['"]$/g; +const trimQuotes = (str: string) => str.replace(QUOTE_REGEX, ''); + +export const convert = (type: PTType): SBType | any => { + const { name, raw, computed, value } = type; + const base: any = {}; + if (typeof raw !== 'undefined') base.raw = raw; + switch (name) { + case 'enum': { + const values = computed ? value : value.map((v: PTType) => trimQuotes(v.value)); + return { ...base, name, value: values }; + } + case 'string': + case 'number': + case 'symbol': + return { ...base, name }; + case 'func': + return { ...base, name: 'function' }; + case 'bool': + return { ...base, name: 'boolean' }; + case 'arrayOf': + case 'array': + return { ...base, name: 'array', value: value && convert(value as PTType) }; + case 'object': + return { ...base, name }; + case 'objectOf': + return { ...base, name, value: convert(value as PTType) }; + case 'shape': + case 'exact': + const values = mapValues(value, (field) => convert(field)); + return { ...base, name: 'object', value: values }; + case 'union': + return { ...base, name: 'union', value: value.map((v: PTType) => convert(v)) }; + case 'instanceOf': + case 'element': + case 'elementType': + default: + const otherVal = value ? `${name}(${value})` : name; + return { ...base, name: 'other', value: otherVal }; + } +}; diff --git a/addons/docs/src/lib/sbtypes/proptypes/index.ts b/addons/docs/src/lib/sbtypes/proptypes/index.ts new file mode 100644 index 000000000000..0ef743fdb7ef --- /dev/null +++ b/addons/docs/src/lib/sbtypes/proptypes/index.ts @@ -0,0 +1,2 @@ +export * from './convert'; +export * from './types'; diff --git a/addons/docs/src/lib/sbtypes/proptypes/types.ts b/addons/docs/src/lib/sbtypes/proptypes/types.ts new file mode 100644 index 000000000000..8f5e2aff2483 --- /dev/null +++ b/addons/docs/src/lib/sbtypes/proptypes/types.ts @@ -0,0 +1,11 @@ +interface PTBaseType { + name: string; + description?: string; + required?: boolean; +} + +export type PTType = PTBaseType & { + value?: any; + raw?: string; + computed?: boolean; +}; diff --git a/addons/docs/src/lib/sbtypes/types.ts b/addons/docs/src/lib/sbtypes/types.ts new file mode 100644 index 000000000000..59d6361aeca7 --- /dev/null +++ b/addons/docs/src/lib/sbtypes/types.ts @@ -0,0 +1,42 @@ +interface SBBaseType { + required?: boolean; + raw?: string; +} + +export type SBScalarType = SBBaseType & { + name: 'boolean' | 'string' | 'number' | 'function'; +}; + +export type SBArrayType = SBBaseType & { + name: 'array'; + value: SBType[]; +}; +export type SBObjectType = SBBaseType & { + name: 'object'; + value: Record; +}; +export type SBEnumType = SBBaseType & { + name: 'enum'; + value: (string | number)[]; +}; +export type SBIntersectionType = SBBaseType & { + name: 'intersection'; + value: SBType[]; +}; +export type SBUnionType = SBBaseType & { + name: 'union'; + value: SBType[]; +}; +export type SBOtherType = SBBaseType & { + name: 'other'; + value: string; +}; + +export type SBType = + | SBScalarType + | SBEnumType + | SBArrayType + | SBObjectType + | SBIntersectionType + | SBUnionType + | SBOtherType; diff --git a/addons/docs/src/lib/sbtypes/typescript/convert.ts b/addons/docs/src/lib/sbtypes/typescript/convert.ts new file mode 100644 index 000000000000..a53d0d79cbce --- /dev/null +++ b/addons/docs/src/lib/sbtypes/typescript/convert.ts @@ -0,0 +1,45 @@ +/* eslint-disable no-case-declarations */ +import { TSType, TSSigType } from './types'; +import { SBType } from '../types'; + +const convertSig = (type: TSSigType) => { + switch (type.type) { + case 'function': + return { name: 'function' }; + case 'object': + const values: any = {}; + type.signature.properties.forEach((prop) => { + values[prop.key] = convert(prop.value); + }); + return { + name: 'object', + value: values, + }; + default: + throw new Error(`Unknown: ${type}`); + } +}; + +export const convert = (type: TSType): SBType | void => { + const { name, raw } = type; + const base: any = {}; + if (typeof raw !== 'undefined') base.raw = raw; + switch (type.name) { + case 'string': + case 'number': + case 'symbol': + case 'boolean': { + return { ...base, name }; + } + case 'Array': { + return { ...base, name: 'array', value: type.elements.map(convert) }; + } + case 'signature': + return { ...base, ...convertSig(type) }; + case 'union': + case 'intersection': + return { ...base, name, value: type.elements.map(convert) }; + default: + return { ...base, name: 'other', value: name }; + } +}; diff --git a/addons/docs/src/lib/sbtypes/typescript/index.ts b/addons/docs/src/lib/sbtypes/typescript/index.ts new file mode 100644 index 000000000000..0ef743fdb7ef --- /dev/null +++ b/addons/docs/src/lib/sbtypes/typescript/index.ts @@ -0,0 +1,2 @@ +export * from './convert'; +export * from './types'; diff --git a/addons/docs/src/lib/sbtypes/typescript/types.ts b/addons/docs/src/lib/sbtypes/typescript/types.ts new file mode 100644 index 000000000000..dee016d76734 --- /dev/null +++ b/addons/docs/src/lib/sbtypes/typescript/types.ts @@ -0,0 +1,46 @@ +interface TSBaseType { + name: string; + type?: string; + raw?: string; + required?: boolean; +} + +type TSArgType = TSType; + +type TSCombinationType = TSBaseType & { + name: 'union' | 'intersection'; + elements: TSType[]; +}; + +type TSFuncSigType = TSBaseType & { + name: 'signature'; + type: 'function'; + signature: { + arguments: TSArgType[]; + return: TSType; + }; +}; + +type TSObjectSigType = TSBaseType & { + name: 'signature'; + type: 'object'; + signature: { + properties: { + key: string; + value: TSType; + }[]; + }; +}; + +type TSScalarType = TSBaseType & { + name: 'any' | 'boolean' | 'number' | 'void' | 'string' | 'symbol'; +}; + +type TSArrayType = TSBaseType & { + name: 'Array'; + elements: TSType[]; +}; + +export type TSSigType = TSObjectSigType | TSFuncSigType; + +export type TSType = TSScalarType | TSCombinationType | TSSigType | TSArrayType; diff --git a/docs/src/pages/formats/component-story-format/index.md b/docs/src/pages/formats/component-story-format/index.md index 4d4a1011d63f..129ac697565f 100644 --- a/docs/src/pages/formats/component-story-format/index.md +++ b/docs/src/pages/formats/component-story-format/index.md @@ -66,6 +66,48 @@ Simple.story = { }; ``` +## Args story inputs + +Starting in SB 6.0, stories accept named inputs called Args. Args are dynamic data that are provided (and possibly updated by) Storybook and its addons. + +Consider Storybook’s ["hello world" example](https://storybook.js.org/docs/basics/writing-stories/#basic-story) of a text button that logs its click events: + +```js +import { action } from '@storybook/addon-actions'; +import { Button } from './Button'; + +export default { title: 'Button', component: Button }; +export const Text = () =>