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 (
+ <>
+
+
+ key
+ val
+
+ {Object.entries(args).map(([key, val]) => (
+
+ {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 = () => ;
+```
+
+Now consider the same example, re-written with args:
+
+```js
+export const Text = ({ label, onClick }) => ;
+Text.story = {
+ args: {
+ label: 'Hello',
+ onClick: action('clicked'),
+ },
+};
+```
+
+At first blush this might seem no better than the original example. However, if we add the [Docs addon](https://github.com/storybookjs/storybook/tree/master/addons/docs) and configure the [Actions addon](https://github.com/storybookjs/storybook/tree/master/addons/actions) appropriately, we can write:
+
+```js
+export const Text = ({ label, onClick }) => ;
+```
+
+Or even more simply:
+
+```js
+export const Text = (args) => ;
+```
+
+Not only are these versions shorter and easier to write than their no-args counterparts, but they are also more portable since the code doesn't depend on the actions addon specifically.
+
+For more information on setting up [Docs](https://github.com/storybookjs/storybook/tree/master/addons/docs) and [Actions](https://github.com/storybookjs/storybook/tree/master/addons/actions), see their respective documentation.
+
## Storybook Export vs Name Handling
Storybook handles named exports and `story.name` slightly differently. When should you use one vs. the other?
diff --git a/examples/official-storybook/main.js b/examples/official-storybook/main.js
index a59822477129..b1a91108b4b1 100644
--- a/examples/official-storybook/main.js
+++ b/examples/official-storybook/main.js
@@ -4,7 +4,7 @@ module.exports = {
'../../lib/ui/src/**/*.stories.(js|tsx|mdx)',
'../../lib/components/src/**/*.stories.(js|tsx|mdx)',
'./stories/**/*.stories.(js|tsx|mdx)',
- './../../addons/docs/**/react-properties.stories.tsx',
+ './../../addons/docs/**/*.stories.tsx',
],
addons: [
'@storybook/addon-docs',
diff --git a/examples/official-storybook/preview.js b/examples/official-storybook/preview.js
index 98da4a2c6db5..28691b093ca7 100644
--- a/examples/official-storybook/preview.js
+++ b/examples/official-storybook/preview.js
@@ -68,12 +68,10 @@ addParameters({
});
export const parameters = {
+ passArgsFirst: true,
exportedParameter: 'exportedParameter',
- args: { invalid1: 'will warn' },
};
-export const args = { invalid2: 'will warn' };
-
export const globalArgs = {
foo: 'fooValue',
};
diff --git a/examples/official-storybook/stories/addon-docs/props.stories.mdx b/examples/official-storybook/stories/addon-docs/props.stories.mdx
new file mode 100644
index 000000000000..87b768ff43e7
--- /dev/null
+++ b/examples/official-storybook/stories/addon-docs/props.stories.mdx
@@ -0,0 +1,94 @@
+import { Meta, Preview, Story, Props } from '@storybook/addon-docs/blocks';
+import { DocgenButton } from '../../components/DocgenButton';
+import { ButtonGroup, SubGroup } from '../../components/ButtonGroup';
+import { ForwardRefButton } from '../../components/ForwardRefButton';
+import { MemoButton } from '../../components/MemoButton';
+
+
+
+export const FooBar = ({ foo, bar } = {}) => (
+
+
+ Foo
+ {foo && foo.toString()}
+
+
+ Bar
+ {bar}
+
+
+);
+
+## ArgTypes
+
+
+
+ {(args) => }
+
+
+
+
+
+## ArgTypes w/ Components
+
+
+
+## Args
+
+
+
+ {(args) => }
+
+
+
+
+
+## Component Story
+
+
+
+ {(args) => }
+
+
+
+
+
+## Controls Story
+
+
+
+ {(args) => }
+
+
+
+
+
+## Components
+
+
+
+## Component
+
+
diff --git a/examples/official-storybook/stories/addon-docs/subcomponents.stories.js b/examples/official-storybook/stories/addon-docs/subcomponents.stories.js
index 0a282af957e7..4ad75e1b5878 100644
--- a/examples/official-storybook/stories/addon-docs/subcomponents.stories.js
+++ b/examples/official-storybook/stories/addon-docs/subcomponents.stories.js
@@ -7,10 +7,15 @@ export default {
component: ButtonGroup,
parameters: { viewMode: 'docs' },
subcomponents: { DocgenButton },
+ argTypes: {
+ background: {
+ control: { type: 'color' },
+ },
+ },
};
-export const basic = () => (
-
+export const Basic = (args) => (
+
diff --git a/lib/client-api/src/story_store.ts b/lib/client-api/src/story_store.ts
index 015c23bc2e64..13fc7c1cff66 100644
--- a/lib/client-api/src/story_store.ts
+++ b/lib/client-api/src/story_store.ts
@@ -288,6 +288,7 @@ export default class StoryStore {
...accumlatedParameters,
argTypes: enhancer({
...identification,
+ storyFn: original,
parameters: accumlatedParameters,
args: {},
globalArgs: {},
diff --git a/lib/components/package.json b/lib/components/package.json
index 7608ebcf52b9..832d9bccc9c7 100644
--- a/lib/components/package.json
+++ b/lib/components/package.json
@@ -31,9 +31,11 @@
"@storybook/client-logger": "6.0.0-alpha.33",
"@storybook/theming": "6.0.0-alpha.33",
"@types/overlayscrollbars": "^1.9.0",
+ "@types/react-color": "^3.0.1",
"@types/react-syntax-highlighter": "11.0.4",
"@types/react-textarea-autosize": "^4.3.3",
"core-js": "^3.0.1",
+ "fast-deep-equal": "^3.1.1",
"global": "^4.3.2",
"lodash": "^4.17.15",
"markdown-to-jsx": "^6.9.1",
@@ -43,9 +45,11 @@
"polished": "^3.4.4",
"popper.js": "^1.14.7",
"react": "^16.8.3",
+ "react-color": "^2.17.0",
"react-dom": "^16.8.3",
"react-helmet-async": "^1.0.2",
"react-popper-tooltip": "^2.11.0",
+ "react-select": "^3.0.8",
"react-syntax-highlighter": "^11.0.2",
"react-textarea-autosize": "^7.1.0",
"ts-dedent": "^1.1.1"
diff --git a/lib/components/src/blocks/ArgsTable/ArgControl.tsx b/lib/components/src/blocks/ArgsTable/ArgControl.tsx
new file mode 100644
index 000000000000..626c603720d0
--- /dev/null
+++ b/lib/components/src/blocks/ArgsTable/ArgControl.tsx
@@ -0,0 +1,58 @@
+import React, { FC, useCallback } from 'react';
+import { Args, ArgType } from './types';
+import {
+ ArrayControl,
+ BooleanControl,
+ ColorControl,
+ DateControl,
+ NumberControl,
+ ObjectControl,
+ OptionsControl,
+ RangeControl,
+ TextControl,
+} from '../../controls';
+
+export interface ArgControlProps {
+ row: ArgType;
+ arg: any;
+ updateArgs: (args: Args) => void;
+}
+
+export const ArgControl: FC = ({ row, arg, updateArgs }) => {
+ const { name, control } = row;
+ const onChange = useCallback(
+ (argName: string, argVal: any) => {
+ updateArgs({ [name]: argVal });
+ return argVal;
+ },
+ [updateArgs, name]
+ );
+
+ if (!control) {
+ return <>->;
+ }
+
+ const props = { name, value: arg, onChange };
+ switch (control.type) {
+ case 'array':
+ return ;
+ case 'boolean':
+ return ;
+ case 'color':
+ return ;
+ case 'date':
+ return ;
+ case 'number':
+ return ;
+ case 'object':
+ return ;
+ case 'options':
+ return ;
+ case 'range':
+ return ;
+ case 'text':
+ return ;
+ default:
+ return null;
+ }
+};
diff --git a/lib/components/src/blocks/ArgsTable/ArgJsDoc.tsx b/lib/components/src/blocks/ArgsTable/ArgJsDoc.tsx
new file mode 100644
index 000000000000..804816c46e67
--- /dev/null
+++ b/lib/components/src/blocks/ArgsTable/ArgJsDoc.tsx
@@ -0,0 +1,98 @@
+import React, { FC } from 'react';
+import { styled } from '@storybook/theming';
+import { JsDocTags } from './types';
+import { codeCommon } from '../../typography/shared';
+
+interface ArgJsDocArgs {
+ tags: JsDocTags;
+}
+
+export const Table = styled.table(({ theme }) => ({
+ '&&': {
+ // Escape default table styles
+ borderCollapse: 'collapse',
+ borderSpacing: 0,
+ border: 'none',
+
+ tr: {
+ border: 'none !important',
+ background: 'none',
+ },
+
+ 'td, th': {
+ padding: 0,
+ border: 'none',
+ width: 'auto!important',
+ },
+ // End escape
+
+ marginTop: 0,
+ marginBottom: 0,
+
+ 'th:first-of-type, td:first-of-type': {
+ paddingLeft: 0,
+ },
+
+ 'th:last-of-type, td:last-of-type': {
+ paddingRight: 0,
+ },
+
+ td: {
+ paddingTop: 0,
+ paddingBottom: 4,
+
+ '&:not(:first-of-type)': {
+ paddingLeft: 10,
+ paddingRight: 0,
+ },
+ },
+
+ tbody: {
+ boxShadow: 'none',
+ border: 'none',
+ },
+
+ code: codeCommon({ theme }),
+
+ '& code': {
+ margin: 0,
+ display: 'inline-block',
+ },
+ },
+}));
+
+export const ArgJsDoc: FC = ({ tags }) => {
+ const params = (tags.params || []).filter((x) => x.description);
+ const hasDisplayableParams = params.length !== 0;
+ const hasDisplayableReturns = tags.returns != null && tags.returns.description != null;
+
+ if (!hasDisplayableParams && !hasDisplayableReturns) {
+ return null;
+ }
+
+ return (
+
+
+ {hasDisplayableParams &&
+ params.map((x) => {
+ return (
+
+
+ {x.name}
+
+ {x.description}
+
+ );
+ })}
+ {hasDisplayableReturns && (
+
+
+ Returns
+
+ {tags.returns.description}
+
+ )}
+
+
+ );
+};
diff --git a/lib/components/src/blocks/ArgsTable/ArgRow.stories.tsx b/lib/components/src/blocks/ArgsTable/ArgRow.stories.tsx
new file mode 100644
index 000000000000..b11fa5b64fcc
--- /dev/null
+++ b/lib/components/src/blocks/ArgsTable/ArgRow.stories.tsx
@@ -0,0 +1,124 @@
+import React from 'react';
+import { ArgRow } from './ArgRow';
+import { TableWrapper } from './ArgsTable';
+import { ResetWrapper } from '../../typography/DocumentFormatting';
+
+export default {
+ component: ArgRow,
+ title: 'Docs/ArgRow',
+ excludeStories: /.*Type$/,
+ decorators: [
+ (getStory) => (
+
+
+ {getStory()}
+
+
+ ),
+ ],
+};
+
+export const stringType = {
+ name: 'someString',
+ description: 'someString description',
+ table: {
+ type: { summary: 'string', required: true },
+ defaultValue: { summary: 'fixme' },
+ },
+};
+
+export const longNameType = {
+ ...stringType,
+ name: 'reallyLongStringThatTakesUpSpace',
+};
+
+export const longDescType = {
+ ...stringType,
+ description: 'really long description that takes up a lot of space. sometimes this happens.',
+};
+
+export const numberType = {
+ name: 'someNumber',
+ description: 'someNumber description',
+ table: {
+ type: { summary: 'number', required: false },
+ defaultValue: { summary: '0' },
+ },
+};
+
+export const objectType = {
+ name: 'someObject',
+ description: 'A simple `objectOf` propType.',
+ table: {
+ type: { summary: 'objectOf(number)', required: false },
+ defaultValue: { summary: '{ key: 1 }' },
+ },
+};
+
+export const arrayType = {
+ name: 'someArray',
+ description: 'array of a certain type',
+ table: {
+ type: { summary: 'number[]', required: false },
+ defaultValue: { summary: '[1, 2, 3]' },
+ },
+};
+
+export const complexType = {
+ name: 'someComplex',
+ description: 'A very complex `objectOf` propType.',
+ table: {
+ type: {
+ summary: 'object',
+ detail: `[{
+ id: number,
+ func: func,
+ arr: [{ index: number }]
+ }]`,
+ required: false,
+ },
+ defaultValue: {
+ summary: 'object',
+ detail: `[{
+ id: 1,
+ func: () => {},
+ arr: [{ index: 1 }]
+ }]`,
+ },
+ },
+};
+
+export const funcType = {
+ name: 'concat',
+ description: 'concat 2 string values.',
+ table: {
+ type: { summary: '(a: string, b: string) => string', required: true },
+ defaultValue: { summary: 'func', detail: '(a, b) => { return a + b; }' },
+ jsDocTags: {
+ params: [
+ { name: 'a', description: 'The first string' },
+ { name: 'b', description: 'The second string' },
+ ],
+ returns: { description: 'The concatenation of both strings' },
+ },
+ },
+};
+
+export const markdownType = {
+ name: 'someString',
+ description:
+ 'A `prop` can *support* __markdown__ syntax. This was ship in ~~5.2~~ 5.3. [Find more info in the storybook docs.](https://storybook.js.org/)',
+ table: {
+ type: { summary: 'string', required: false },
+ },
+};
+
+export const string = () => ;
+export const longName = () => ;
+export const longDesc = () => ;
+export const number = () => ;
+export const objectOf = () => ;
+export const arrayOf = () => ;
+export const complexObject = () => ;
+export const func = () => ;
+export const markdown = () => ;
diff --git a/lib/components/src/blocks/ArgsTable/ArgRow.tsx b/lib/components/src/blocks/ArgsTable/ArgRow.tsx
new file mode 100644
index 000000000000..a43dc46b9a20
--- /dev/null
+++ b/lib/components/src/blocks/ArgsTable/ArgRow.tsx
@@ -0,0 +1,101 @@
+import React, { FC } from 'react';
+import Markdown from 'markdown-to-jsx';
+import { transparentize } from 'polished';
+import { styled } from '@storybook/theming';
+import { TableArgType, Args, TableAnnotation } from './types';
+import { ArgJsDoc } from './ArgJsDoc';
+import { ArgValue } from './ArgValue';
+import { ArgControl, ArgControlProps } from './ArgControl';
+import { codeCommon } from '../../typography/shared';
+
+export interface ArgRowProps {
+ row: TableArgType;
+ arg: any;
+ updateArgs?: (args: Args) => void;
+}
+
+const Name = styled.span({ fontWeight: 'bold' });
+
+const Required = styled.span(({ theme }) => ({
+ color: theme.color.negative,
+ fontFamily: theme.typography.fonts.mono,
+ cursor: 'help',
+}));
+
+const Description = styled.div(({ theme }) => ({
+ '&&': {
+ p: {
+ margin: '0 0 10px 0',
+ },
+ },
+
+ code: codeCommon({ theme }),
+
+ '& code': {
+ margin: 0,
+ display: 'inline-block',
+ },
+}));
+
+const Type = styled.div<{ hasDescription: boolean }>(({ theme, hasDescription }) => ({
+ color:
+ theme.base === 'light'
+ ? transparentize(0.1, theme.color.defaultText)
+ : transparentize(0.2, theme.color.defaultText),
+ marginTop: hasDescription ? 4 : 0,
+}));
+
+const TypeWithJsDoc = styled.div<{ hasDescription: boolean }>(({ theme, hasDescription }) => ({
+ color:
+ theme.base === 'light'
+ ? transparentize(0.1, theme.color.defaultText)
+ : transparentize(0.2, theme.color.defaultText),
+ marginTop: hasDescription ? 12 : 0,
+ marginBottom: 12,
+}));
+
+export const ArgRow: FC = (props) => {
+ const { row, arg, updateArgs } = props;
+ const { name, description } = row;
+ const table = (row.table || {}) as TableAnnotation;
+ const type = table.type || row.type;
+ const defaultValue = table.defaultValue || row.defaultValue;
+ const required = type?.required;
+ const hasDescription = description != null && description !== '';
+
+ return (
+
+
+ {name}
+ {required ? * : null}
+
+
+ {hasDescription && (
+
+ {description}
+
+ )}
+ {table.jsDocTags != null ? (
+ <>
+
+
+
+
+ >
+ ) : (
+
+
+
+ )}
+
+
+
+
+ {updateArgs ? (
+
+
+
+ ) : null}
+
+ );
+};
diff --git a/lib/components/src/blocks/ArgsTable/ArgValue.tsx b/lib/components/src/blocks/ArgsTable/ArgValue.tsx
new file mode 100644
index 000000000000..b76c763e6c37
--- /dev/null
+++ b/lib/components/src/blocks/ArgsTable/ArgValue.tsx
@@ -0,0 +1,114 @@
+import React, { FC, useState } from 'react';
+import { styled } from '@storybook/theming';
+import memoize from 'memoizerific';
+import { PropSummaryValue } from './types';
+import { WithTooltipPure } from '../../tooltip/WithTooltip';
+import { Icons } from '../../icon/icon';
+import { SyntaxHighlighter } from '../../syntaxhighlighter/syntaxhighlighter';
+import { codeCommon } from '../../typography/shared';
+
+interface ArgValueProps {
+ value?: PropSummaryValue;
+}
+
+interface ArgTextProps {
+ text: string;
+}
+
+interface ArgSummaryProps {
+ value: PropSummaryValue;
+}
+
+const Text = styled.span(({ theme }) => ({
+ fontFamily: theme.typography.fonts.mono,
+ fontSize: theme.typography.size.s2 - 1,
+}));
+
+const Expandable = styled.div<{}>(codeCommon, ({ theme }) => ({
+ fontFamily: theme.typography.fonts.mono,
+ color: theme.color.secondary,
+ margin: 0,
+ whiteSpace: 'nowrap',
+ display: 'flex',
+ alignItems: 'center',
+}));
+
+const Detail = styled.div<{ width: string }>(({ theme, width }) => ({
+ width,
+ minWidth: 200,
+ maxWidth: 800,
+ padding: 15,
+ // Dont remove the mono fontFamily here even if it seem useless, this is used by the browser to calculate the length of a "ch" unit.
+ fontFamily: theme.typography.fonts.mono,
+ fontSize: theme.typography.size.s2 - 1,
+ // Most custom stylesheet will reset the box-sizing to "border-box" and will break the tooltip.
+ boxSizing: 'content-box',
+
+ '& code': {
+ padding: '0 !important',
+ },
+}));
+
+const ArrowIcon = styled(Icons)({
+ height: 10,
+ width: 10,
+ minWidth: 10,
+ marginLeft: 4,
+});
+
+const EmptyArg = () => {
+ return - ;
+};
+
+const ArgText: FC = ({ text }) => {
+ return {text} ;
+};
+
+const calculateDetailWidth = memoize(1000)((detail: string): string => {
+ const lines = detail.split(/\r?\n/);
+
+ return `${Math.max(...lines.map((x) => x.length))}ch`;
+});
+
+const ArgSummary: FC = ({ value }) => {
+ const { summary, detail } = value;
+
+ const [isOpen, setIsOpen] = useState(false);
+ // summary is used for the default value
+ // below check fixes not displaying default values for boolean typescript vars
+ const summaryAsString =
+ summary !== undefined && summary !== null && typeof summary.toString === 'function'
+ ? summary.toString()
+ : summary;
+ if (detail == null) {
+ return ;
+ }
+
+ return (
+ {
+ setIsOpen(isVisible);
+ }}
+ tooltip={
+
+
+ {detail}
+
+
+ }
+ >
+
+ {summaryAsString}
+
+
+
+ );
+};
+
+export const ArgValue: FC = ({ value }) => {
+ return value == null ? : ;
+};
diff --git a/lib/components/src/blocks/ArgsTable/ArgsTable.stories.tsx b/lib/components/src/blocks/ArgsTable/ArgsTable.stories.tsx
new file mode 100644
index 000000000000..3525cb45bf71
--- /dev/null
+++ b/lib/components/src/blocks/ArgsTable/ArgsTable.stories.tsx
@@ -0,0 +1,27 @@
+import React from 'react';
+import { ArgsTable, ArgsTableError } from './ArgsTable';
+import { stringType, numberType } from './ArgRow.stories';
+
+export default {
+ component: ArgsTable,
+ title: 'Docs/ArgsTable',
+};
+
+const propsSection = { category: 'props ' };
+const eventsSection = { category: 'events ' };
+
+export const normal = () => ;
+
+export const sections = () => (
+
+);
+
+export const error = () => ;
+
+export const empty = () => ;
diff --git a/lib/components/src/blocks/ArgsTable/ArgsTable.tsx b/lib/components/src/blocks/ArgsTable/ArgsTable.tsx
new file mode 100644
index 000000000000..464df50e1ce0
--- /dev/null
+++ b/lib/components/src/blocks/ArgsTable/ArgsTable.tsx
@@ -0,0 +1,230 @@
+import React, { FC } from 'react';
+import { styled } from '@storybook/theming';
+import { opacify, transparentize, darken, lighten } from 'polished';
+import { ArgRow, ArgRowProps } from './ArgRow';
+import { SectionRow, SectionRowProps } from './SectionRow';
+import { ArgTypes, Args } from './types';
+import { EmptyBlock } from '../EmptyBlock';
+import { ResetWrapper } from '../../typography/DocumentFormatting';
+
+export const TableWrapper = styled.table<{}>(({ theme }) => ({
+ '&&': {
+ // Resets for cascading/system styles
+ borderCollapse: 'collapse',
+ borderSpacing: 0,
+ color: theme.color.defaultText,
+ tr: {
+ border: 'none',
+ background: 'none',
+ },
+
+ 'td, th': {
+ padding: 0,
+ border: 'none',
+ verticalAlign: 'top',
+ },
+ // End Resets
+
+ fontSize: theme.typography.size.s2,
+ lineHeight: '20px',
+ textAlign: 'left',
+ width: '100%',
+
+ // Margin collapse
+ marginTop: 25,
+ marginBottom: 40,
+
+ 'th:first-of-type, td:first-of-type': {
+ paddingLeft: 20,
+ },
+
+ 'th:last-of-type, td:last-of-type': {
+ paddingRight: 20,
+ width: '20%',
+ },
+
+ th: {
+ color:
+ theme.base === 'light'
+ ? transparentize(0.25, theme.color.defaultText)
+ : transparentize(0.45, theme.color.defaultText),
+ paddingTop: 10,
+ paddingBottom: 10,
+
+ '&:not(:first-of-type)': {
+ paddingLeft: 15,
+ paddingRight: 15,
+ },
+ },
+
+ td: {
+ paddingTop: '16px',
+ paddingBottom: '16px',
+
+ '&:not(:first-of-type)': {
+ paddingLeft: 15,
+ paddingRight: 15,
+ },
+
+ '&:last-of-type': {
+ paddingRight: 20,
+ },
+ },
+
+ // Table "block" styling
+ // Emphasize tbody's background and set borderRadius
+ // Calling out because styling tables is finicky
+
+ // Makes border alignment consistent w/other DocBlocks
+ marginLeft: 1,
+ marginRight: 1,
+
+ 'tr:first-child': {
+ 'td:first-child, th:first-child': {
+ borderTopLeftRadius: theme.appBorderRadius,
+ },
+ 'td:last-child, th:last-child': {
+ borderTopRightRadius: theme.appBorderRadius,
+ },
+ },
+
+ 'tr:last-child': {
+ 'td:first-child, th:first-child': {
+ borderBottomLeftRadius: theme.appBorderRadius,
+ },
+ 'td:last-child, th:last-child': {
+ borderBottomRightRadius: theme.appBorderRadius,
+ },
+ },
+
+ tbody: {
+ // slightly different than the other DocBlock shadows to account for table styling gymnastics
+ boxShadow:
+ theme.base === 'light'
+ ? `rgba(0, 0, 0, 0.10) 0 1px 3px 1px,
+ ${transparentize(0.035, theme.appBorderColor)} 0 0 0 1px`
+ : `rgba(0, 0, 0, 0.20) 0 2px 5px 1px,
+ ${opacify(0.05, theme.appBorderColor)} 0 0 0 1px`,
+ borderRadius: theme.appBorderRadius,
+
+ tr: {
+ background: 'transparent',
+ overflow: 'hidden',
+ '&:not(:first-child)': {
+ borderTopWidth: 1,
+ borderTopStyle: 'solid',
+ borderTopColor:
+ theme.base === 'light'
+ ? darken(0.1, theme.background.content)
+ : lighten(0.05, theme.background.content),
+ },
+ },
+
+ td: {
+ background: theme.background.content,
+ },
+ },
+ // End finicky table styling
+ },
+}));
+
+export enum ArgsTableError {
+ NO_COMPONENT = 'No component found',
+ ARGS_UNSUPPORTED = 'Args unsupported. See Args documentation for your framework.',
+}
+
+export interface ArgsTableRowProps {
+ rows: ArgTypes;
+ args?: Args;
+ updateArgs?: (args: Args) => void;
+}
+
+export interface ArgsTableErrorProps {
+ error: ArgsTableError;
+}
+
+export type ArgsTableProps = ArgsTableRowProps | ArgsTableErrorProps;
+type RowProps = SectionRowProps | ArgRowProps;
+
+const ArgsTableRow: FC = (props) => {
+ const { section, showControls } = props as SectionRowProps;
+ if (section) {
+ return ;
+ }
+ const { row, arg, updateArgs } = props as ArgRowProps;
+ return ;
+};
+
+/**
+ * Display the props for a component as a props table. Each row is a collection of
+ * ArgDefs, usually derived from docgen info for the component.
+ */
+export const ArgsTable: FC = (props) => {
+ const { error } = props as ArgsTableErrorProps;
+ if (error) {
+ return {error} ;
+ }
+
+ const { rows, args, updateArgs } = props as ArgsTableRowProps;
+
+ const ungroupedRows: ArgTypes = {};
+ const categoryRows: Record = {};
+ if (rows) {
+ Object.entries(rows).forEach(([key, row]) => {
+ const { table: { category = null } = {} } = row;
+ if (category) {
+ if (!categoryRows[category]) categoryRows[category] = {};
+ categoryRows[category][key] = row;
+ } else {
+ ungroupedRows[key] = row;
+ }
+ });
+ }
+
+ const showControls = args && Object.keys(args).length > 0;
+
+ const allRows: { key: string; value: any }[] = [];
+ Object.entries(ungroupedRows).forEach(([key, row]) => {
+ const arg = args && args[key];
+ allRows.push({
+ key,
+ value: { row, arg, updateArgs },
+ });
+ });
+ Object.keys(categoryRows).forEach((category) => {
+ const catRows = categoryRows[category];
+ if (Object.keys(catRows).length > 0) {
+ allRows.push({ key: category, value: { section: category, showControls } });
+ Object.entries(catRows).forEach(([key, row]) => {
+ const arg = args && args[key];
+ allRows.push({
+ key: `${category}_${key}`,
+ value: { row, arg, updateArgs },
+ });
+ });
+ }
+ });
+
+ if (allRows.length === 0) {
+ return No props found for this component ;
+ }
+ return (
+
+
+
+
+ Name
+ Description
+ Default
+ {showControls ? Control : null}
+
+
+
+ {allRows.map((row) => (
+
+ ))}
+
+
+
+ );
+};
diff --git a/lib/components/src/blocks/ArgsTable/SectionRow.stories.tsx b/lib/components/src/blocks/ArgsTable/SectionRow.stories.tsx
new file mode 100644
index 000000000000..4347a6bf15f0
--- /dev/null
+++ b/lib/components/src/blocks/ArgsTable/SectionRow.stories.tsx
@@ -0,0 +1,20 @@
+import React from 'react';
+import { SectionRow } from './SectionRow';
+import { TableWrapper } from './ArgsTable';
+import { ResetWrapper } from '../../typography/DocumentFormatting';
+
+export default {
+ component: SectionRow,
+ title: 'Docs/SectionRow',
+ decorators: [
+ (getStory) => (
+
+
+ {getStory()}
+
+
+ ),
+ ],
+};
+
+export const props = () => ;
diff --git a/lib/components/src/blocks/ArgsTable/SectionRow.tsx b/lib/components/src/blocks/ArgsTable/SectionRow.tsx
new file mode 100644
index 000000000000..280d9e3c6ebb
--- /dev/null
+++ b/lib/components/src/blocks/ArgsTable/SectionRow.tsx
@@ -0,0 +1,27 @@
+import React, { FC } from 'react';
+import { transparentize } from 'polished';
+import { styled } from '@storybook/theming';
+
+export interface SectionRowProps {
+ section: string;
+ showControls: boolean;
+}
+
+const SectionTh = styled.th<{}>(({ theme }) => ({
+ letterSpacing: '0.35em',
+ textTransform: 'uppercase',
+ fontWeight: theme.typography.weight.black,
+ fontSize: theme.typography.size.s1 - 1,
+ lineHeight: '24px',
+ color:
+ theme.base === 'light'
+ ? transparentize(0.4, theme.color.defaultText)
+ : transparentize(0.6, theme.color.defaultText),
+ background: `${theme.background.app} !important`,
+}));
+
+export const SectionRow: FC = ({ section, showControls }) => (
+
+ {section}
+
+);
diff --git a/lib/components/src/blocks/ArgsTable/TabbedArgsTable.tsx b/lib/components/src/blocks/ArgsTable/TabbedArgsTable.tsx
new file mode 100644
index 000000000000..249c3ea7bcca
--- /dev/null
+++ b/lib/components/src/blocks/ArgsTable/TabbedArgsTable.tsx
@@ -0,0 +1,31 @@
+import React, { FC } from 'react';
+import { ArgsTable, ArgsTableProps } from './ArgsTable';
+import { TabsState } from '../../tabs/tabs';
+
+export interface TabbedArgsTableProps {
+ tabs: Record;
+}
+
+export const TabbedArgsTable: FC = ({ tabs }) => {
+ const entries = Object.entries(tabs);
+
+ if (entries.length === 1) {
+ return ;
+ }
+
+ return (
+
+ {entries.map((entry) => {
+ const [label, table] = entry;
+ const id = `prop_table_div_${label}`;
+ return (
+
+ {({ active }: { active: boolean }) =>
+ active ?
: null
+ }
+
+ );
+ })}
+
+ );
+};
diff --git a/lib/components/src/blocks/ArgsTable/index.ts b/lib/components/src/blocks/ArgsTable/index.ts
new file mode 100644
index 000000000000..3be4e9b364cf
--- /dev/null
+++ b/lib/components/src/blocks/ArgsTable/index.ts
@@ -0,0 +1,3 @@
+export * from './types';
+export * from './ArgsTable';
+export * from './TabbedArgsTable';
diff --git a/lib/components/src/blocks/ArgsTable/types.ts b/lib/components/src/blocks/ArgsTable/types.ts
new file mode 100644
index 000000000000..6d1f67e08e7e
--- /dev/null
+++ b/lib/components/src/blocks/ArgsTable/types.ts
@@ -0,0 +1,48 @@
+export interface JsDocParam {
+ name: string;
+ description?: string;
+}
+
+export interface JsDocReturns {
+ description?: string;
+}
+
+export interface JsDocTags {
+ params?: JsDocParam[];
+ returns?: JsDocReturns;
+}
+
+export interface PropSummaryValue {
+ summary: string;
+ detail?: string;
+ required?: boolean;
+}
+
+export type PropType = PropSummaryValue;
+export type PropDefaultValue = PropSummaryValue;
+
+export interface ArgType {
+ name?: string;
+ description?: string;
+ defaultValue?: any;
+ [key: string]: any;
+}
+
+export interface TableAnnotation {
+ type: PropType;
+ jsDocTags?: JsDocTags;
+ defaultValue?: PropDefaultValue;
+ category?: string;
+}
+
+export type TableArgType = ArgType & {
+ table: TableAnnotation;
+};
+
+export interface ArgTypes {
+ [key: string]: ArgType;
+}
+
+export interface Args {
+ [key: string]: any;
+}
diff --git a/lib/components/src/blocks/PropsTable/PropDef.ts b/lib/components/src/blocks/PropsTable/PropDef.ts
index 441f2da2540a..d81a85334e20 100644
--- a/lib/components/src/blocks/PropsTable/PropDef.ts
+++ b/lib/components/src/blocks/PropsTable/PropDef.ts
@@ -23,6 +23,7 @@ export type PropDefaultValue = PropSummaryValue;
export interface PropDef {
name: string;
type: PropType;
+ sbType?: any;
required: boolean;
description?: string;
defaultValue?: PropDefaultValue;
diff --git a/lib/components/src/blocks/PropsTable/PropsTable.tsx b/lib/components/src/blocks/PropsTable/PropsTable.tsx
index 5352a05cdc12..3c71cbd06d65 100644
--- a/lib/components/src/blocks/PropsTable/PropsTable.tsx
+++ b/lib/components/src/blocks/PropsTable/PropsTable.tsx
@@ -3,7 +3,7 @@ import { styled } from '@storybook/theming';
import { opacify, transparentize, darken, lighten } from 'polished';
import { PropRow, PropRowProps } from './PropRow';
import { SectionRow, SectionRowProps } from './SectionRow';
-import { PropDef, PropType, PropDefaultValue, PropSummaryValue } from './PropDef';
+import { PropDef } from './PropDef';
import { EmptyBlock } from '../EmptyBlock';
import { ResetWrapper } from '../../typography/DocumentFormatting';
@@ -212,4 +212,4 @@ const PropsTable: FC = (props) => {
);
};
-export { PropsTable, PropDef, PropType, PropDefaultValue, PropSummaryValue };
+export { PropsTable, PropDef };
diff --git a/lib/components/src/blocks/PropsTable/index.ts b/lib/components/src/blocks/PropsTable/index.ts
new file mode 100644
index 000000000000..165798d44676
--- /dev/null
+++ b/lib/components/src/blocks/PropsTable/index.ts
@@ -0,0 +1 @@
+export * from './PropsTable';
diff --git a/lib/components/src/blocks/index.ts b/lib/components/src/blocks/index.ts
index 3020d6ec5700..1c17ed6d2374 100644
--- a/lib/components/src/blocks/index.ts
+++ b/lib/components/src/blocks/index.ts
@@ -1,7 +1,8 @@
export * from './Description';
export * from './DocsPage';
export * from './Preview';
-export * from './PropsTable/PropsTable';
+export * from './PropsTable';
+export * from './ArgsTable';
export * from './Source';
export * from './Story';
export * from './IFrame';
diff --git a/lib/components/src/controls/Array.stories.tsx b/lib/components/src/controls/Array.stories.tsx
new file mode 100644
index 000000000000..ff83cb0679bb
--- /dev/null
+++ b/lib/components/src/controls/Array.stories.tsx
@@ -0,0 +1,27 @@
+import React, { useState } from 'react';
+import { ArrayControl } from './Array';
+
+export default {
+ title: 'Controls/Array',
+ component: ArrayControl,
+};
+
+export const Basic = () => {
+ const [value, setValue] = useState(['Bat', 'Cat', 'Rat']);
+ return (
+ <>
+ setValue(newVal)} />
+ {value && value.map((item) => {item} )}
+ >
+ );
+};
+
+export const Null = () => {
+ const [value, setValue] = useState(null);
+ return (
+ <>
+ setValue(newVal)} />
+ {value && value.map((item) => {item} )}
+ >
+ );
+};
diff --git a/lib/components/src/controls/Array.tsx b/lib/components/src/controls/Array.tsx
new file mode 100644
index 000000000000..3ba367a6615d
--- /dev/null
+++ b/lib/components/src/controls/Array.tsx
@@ -0,0 +1,32 @@
+import React, { FC, ChangeEvent, useCallback } from 'react';
+
+import { Form } from '../form';
+import { ControlProps, ArrayValue, ArrayConfig } from './types';
+
+const parse = (value: string, separator: string): ArrayValue =>
+ !value || value.trim() === '' ? [] : value.split(separator);
+
+const format = (value: ArrayValue, separator: string) => {
+ return value ? value.join(separator) : '';
+};
+
+export type ArrayProps = ControlProps & ArrayConfig;
+export const ArrayControl: FC = ({ name, value, onChange, separator = ',' }) => {
+ const handleChange = useCallback(
+ (e: ChangeEvent): void => {
+ const { value: newVal } = e.target;
+ onChange(name, parse(newVal, separator));
+ },
+ [onChange]
+ );
+
+ return (
+
+ );
+};
diff --git a/lib/components/src/controls/Boolean.stories.tsx b/lib/components/src/controls/Boolean.stories.tsx
new file mode 100644
index 000000000000..c0bd2bc53c2d
--- /dev/null
+++ b/lib/components/src/controls/Boolean.stories.tsx
@@ -0,0 +1,17 @@
+import React, { useState } from 'react';
+import { BooleanControl } from './Boolean';
+
+export default {
+ title: 'Controls/Boolean',
+ component: BooleanControl,
+};
+
+export const Basic = () => {
+ const [value, setValue] = useState(false);
+ return (
+ <>
+ setValue(newVal)} />
+ value: {value.toString()}
+ >
+ );
+};
diff --git a/lib/components/src/controls/Boolean.tsx b/lib/components/src/controls/Boolean.tsx
new file mode 100644
index 000000000000..7cc22467c3b7
--- /dev/null
+++ b/lib/components/src/controls/Boolean.tsx
@@ -0,0 +1,29 @@
+import React, { FC } from 'react';
+
+import { styled } from '@storybook/theming';
+import { ControlProps, BooleanValue, BooleanConfig } from './types';
+
+const Input = styled.input({
+ display: 'table-cell',
+ boxSizing: 'border-box',
+ verticalAlign: 'top',
+ height: 21,
+ outline: 'none',
+ border: '1px solid #ececec',
+ fontSize: '12px',
+ color: '#555',
+});
+
+const format = (value: BooleanValue): string | null => (value ? String(value) : null);
+const parse = (value: string | null) => value === 'true';
+
+export type BooleanProps = ControlProps & BooleanConfig;
+export const BooleanControl: FC = ({ name, value, onChange }) => (
+ onChange(name, e.target.checked)}
+ checked={value}
+ />
+);
diff --git a/lib/components/src/controls/Color.stories.tsx b/lib/components/src/controls/Color.stories.tsx
new file mode 100644
index 000000000000..852f528b6106
--- /dev/null
+++ b/lib/components/src/controls/Color.stories.tsx
@@ -0,0 +1,12 @@
+import React, { useState } from 'react';
+import { ColorControl } from './Color';
+
+export default {
+ title: 'Controls/Color',
+ component: ColorControl,
+};
+
+export const Basic = () => {
+ const [value, setValue] = useState('#ff0');
+ return setValue(newVal)} />;
+};
diff --git a/lib/components/src/controls/Color.tsx b/lib/components/src/controls/Color.tsx
new file mode 100644
index 000000000000..fdb17b69a34c
--- /dev/null
+++ b/lib/components/src/controls/Color.tsx
@@ -0,0 +1,56 @@
+import React, { FC, useState } from 'react';
+import { SketchPicker, ColorResult } from 'react-color';
+
+import { styled } from '@storybook/theming';
+import { Form } from '../form';
+import { ControlProps, ColorValue, ColorConfig } from './types';
+
+const Swatch = styled.div<{}>(({ theme }) => ({
+ position: 'absolute',
+ top: '50%',
+ transform: 'translateY(-50%)',
+ left: 6,
+ width: 16,
+ height: 16,
+ boxShadow: `${theme.appBorderColor} 0 0 0 1px inset`,
+ borderRadius: '1rem',
+}));
+
+const ColorButton = styled(Form.Button)<{ active: boolean }>(({ active }) => ({
+ zIndex: active ? 3 : 'unset',
+ width: '100%',
+}));
+
+const Popover = styled.div({
+ position: 'absolute',
+ zIndex: 2,
+});
+
+const format = (color: ColorResult) =>
+ `rgba(${color.rgb.r},${color.rgb.g},${color.rgb.b},${color.rgb.a})`;
+
+export type ColorProps = ControlProps & ColorConfig;
+export const ColorControl: FC = ({ name, value, onChange }) => {
+ const [showPicker, setShowPicker] = useState(false);
+
+ return (
+ setShowPicker(!showPicker)}
+ size="flex"
+ >
+ {value && value.toUpperCase()}
+
+ {showPicker ? (
+
+ onChange(name, format(color))}
+ />
+
+ ) : null}
+
+ );
+};
diff --git a/lib/components/src/controls/Date.stories.tsx b/lib/components/src/controls/Date.stories.tsx
new file mode 100644
index 000000000000..3ae0d5c1136f
--- /dev/null
+++ b/lib/components/src/controls/Date.stories.tsx
@@ -0,0 +1,17 @@
+import React, { useState } from 'react';
+import { DateControl } from './Date';
+
+export default {
+ title: 'Controls/Date',
+ component: DateControl,
+};
+
+export const Basic = () => {
+ const [value, setValue] = useState(new Date());
+ return (
+ <>
+ setValue(newVal)} />
+ {value && new Date(value).toISOString()}
+ >
+ );
+};
diff --git a/lib/components/src/controls/Date.tsx b/lib/components/src/controls/Date.tsx
new file mode 100644
index 000000000000..3e593f727410
--- /dev/null
+++ b/lib/components/src/controls/Date.tsx
@@ -0,0 +1,108 @@
+import React, { FC, ChangeEvent, RefObject, useState, useRef, useEffect } from 'react';
+import { styled } from '@storybook/theming';
+
+import { Form } from '../form';
+import { ControlProps, DateValue, DateConfig } from './types';
+
+const parseDate = (value: string) => {
+ const [year, month, day] = value.split('-');
+ const result = new Date();
+ result.setFullYear(parseInt(year, 10));
+ result.setMonth(parseInt(month, 10) - 1);
+ result.setDate(parseInt(day, 10));
+ return result;
+};
+
+const parseTime = (value: string) => {
+ const [hours, minutes] = value.split(':');
+ const result = new Date();
+ result.setHours(parseInt(hours, 10));
+ result.setMinutes(parseInt(minutes, 10));
+ return result;
+};
+
+const formatDate = (value: Date | number) => {
+ const date = new Date(value);
+ const year = `000${date.getFullYear()}`.slice(-4);
+ const month = `0${date.getMonth() + 1}`.slice(-2);
+ const day = `0${date.getDate()}`.slice(-2);
+ return `${year}-${month}-${day}`;
+};
+
+const formatTime = (value: Date | number) => {
+ const date = new Date(value);
+ const hours = `0${date.getHours()}`.slice(-2);
+ const minutes = `0${date.getMinutes()}`.slice(-2);
+ return `${hours}:${minutes}`;
+};
+
+const FlexSpaced = styled.div({
+ flex: 1,
+ display: 'flex',
+ '&& > *': {
+ marginLeft: 10,
+ },
+ '&& > *:first-of-type': {
+ marginLeft: 0,
+ },
+});
+const FlexInput = styled(Form.Input)({ flex: 1 });
+
+export type DateProps = ControlProps & DateConfig;
+export const DateControl: FC = ({ name, value, onChange }) => {
+ const [valid, setValid] = useState(true);
+ const dateRef = useRef();
+ const timeRef = useRef();
+ useEffect(() => {
+ if (valid !== false) {
+ if (dateRef && dateRef.current) {
+ dateRef.current.value = formatDate(value);
+ }
+ if (timeRef && timeRef.current) {
+ timeRef.current.value = formatTime(value);
+ }
+ }
+ }, [value]);
+
+ const onDateChange = (e: ChangeEvent) => {
+ const parsed = parseDate(e.target.value);
+ const result = new Date(value);
+ result.setFullYear(parsed.getFullYear());
+ result.setMonth(parsed.getMonth());
+ result.setDate(parsed.getDate());
+ const time = result.getTime();
+ if (time) onChange(name, time);
+ setValid(!!time);
+ };
+
+ const onTimeChange = (e: ChangeEvent) => {
+ const parsed = parseTime(e.target.value);
+ const result = new Date(value);
+ result.setHours(parsed.getHours());
+ result.setMinutes(parsed.getMinutes());
+ const time = result.getTime();
+ if (time) onChange(name, time);
+ setValid(!!time);
+ };
+
+ return (
+
+ }
+ id={`${name}date`}
+ name={`${name}date`}
+ onChange={onDateChange}
+ />
+ }
+ onChange={onTimeChange}
+ />
+ {!valid ? invalid
: null}
+
+ );
+};
diff --git a/lib/components/src/controls/Number.stories.tsx b/lib/components/src/controls/Number.stories.tsx
new file mode 100644
index 000000000000..b15b7a1b4849
--- /dev/null
+++ b/lib/components/src/controls/Number.stories.tsx
@@ -0,0 +1,17 @@
+import React, { useState } from 'react';
+import { NumberControl } from './Number';
+
+export default {
+ title: 'Controls/Number',
+ component: NumberControl,
+};
+
+export const Basic = () => {
+ const [value, setValue] = useState(10);
+ return (
+ <>
+ setValue(newVal)} />
+ {value}
+ >
+ );
+};
diff --git a/lib/components/src/controls/Number.tsx b/lib/components/src/controls/Number.tsx
new file mode 100644
index 000000000000..36ec9849c7d6
--- /dev/null
+++ b/lib/components/src/controls/Number.tsx
@@ -0,0 +1,32 @@
+import React, { FC, ChangeEvent } from 'react';
+
+import { Form } from '../form';
+import { ControlProps, NumberValue, NumberConfig } from './types';
+
+type NumberProps = ControlProps & NumberConfig;
+
+export const parse = (value: string) => {
+ const result = parseFloat(value);
+ return Number.isNaN(result) ? null : result;
+};
+
+export const format = (value: NumberValue) => (value != null ? String(value) : '');
+
+export const NumberControl: FC = ({ name, value, onChange, min, max, step }) => {
+ const handleChange = (event: ChangeEvent) => {
+ onChange(name, parse(event.target.value));
+ };
+
+ return (
+
+ );
+};
diff --git a/lib/components/src/controls/Object.stories.tsx b/lib/components/src/controls/Object.stories.tsx
new file mode 100644
index 000000000000..d4808bbf4e4a
--- /dev/null
+++ b/lib/components/src/controls/Object.stories.tsx
@@ -0,0 +1,27 @@
+import React, { useState } from 'react';
+import { ObjectControl } from './Object';
+
+export default {
+ title: 'Controls/Object',
+ component: ObjectControl,
+};
+
+export const Basic = () => {
+ const [value, setValue] = useState({ name: 'Michael', nested: { something: true } });
+ return (
+ <>
+ setValue(newVal)} />
+ {value && JSON.stringify(value)}
+ >
+ );
+};
+
+export const Null = () => {
+ const [value, setValue] = useState(null);
+ return (
+ <>
+ setValue(newVal)} />
+ {value && JSON.stringify(value)}
+ >
+ );
+};
diff --git a/lib/components/src/controls/Object.tsx b/lib/components/src/controls/Object.tsx
new file mode 100644
index 000000000000..9bf2934379c8
--- /dev/null
+++ b/lib/components/src/controls/Object.tsx
@@ -0,0 +1,43 @@
+import React, { FC, ChangeEvent, useState, useCallback } from 'react';
+import deepEqual from 'fast-deep-equal';
+import { Form } from '../form';
+import { ControlProps, ObjectValue, ObjectConfig } from './types';
+
+const format = (value: any) => (value ? JSON.stringify(value) : '');
+
+const parse = (value: string) => {
+ const trimmed = value && value.trim();
+ return trimmed ? JSON.parse(trimmed) : {};
+};
+
+export type ObjectProps = ControlProps & ObjectConfig;
+export const ObjectControl: FC = ({ name, value, onChange }) => {
+ const [valid, setValid] = useState(true);
+ const [text, setText] = useState(format(value));
+
+ const handleChange = useCallback(
+ (e: ChangeEvent) => {
+ try {
+ const newVal = parse(e.target.value);
+ if (!deepEqual(value, newVal)) {
+ onChange(name, newVal);
+ }
+ setValid(true);
+ } catch (err) {
+ setValid(false);
+ }
+ setText(e.target.value);
+ },
+ [onChange, setValid]
+ );
+
+ return (
+
+ );
+};
diff --git a/lib/components/src/controls/Range.stories.tsx b/lib/components/src/controls/Range.stories.tsx
new file mode 100644
index 000000000000..4d8c36f554c7
--- /dev/null
+++ b/lib/components/src/controls/Range.stories.tsx
@@ -0,0 +1,24 @@
+import React, { useState } from 'react';
+import { RangeControl } from './Range';
+
+export default {
+ title: 'Controls/Range',
+ component: RangeControl,
+};
+
+export const Basic = () => {
+ const [value, setValue] = useState(10);
+ return (
+ <>
+ setValue(newVal)}
+ min={0}
+ max={20}
+ step={2}
+ />
+ {value}
+ >
+ );
+};
diff --git a/lib/components/src/controls/Range.tsx b/lib/components/src/controls/Range.tsx
new file mode 100644
index 000000000000..928c1de6e116
--- /dev/null
+++ b/lib/components/src/controls/Range.tsx
@@ -0,0 +1,65 @@
+import React, { FC, ChangeEvent } from 'react';
+
+import { styled } from '@storybook/theming';
+import { ControlProps, NumberValue, RangeConfig } from './types';
+import { parse } from './Number';
+
+type RangeProps = ControlProps & RangeConfig;
+
+const RangeInput = styled.input(
+ {
+ boxSizing: 'border-box',
+ height: 25,
+ outline: 'none',
+ border: '1px solid #f7f4f4',
+ borderRadius: 2,
+ fontSize: 11,
+ padding: 5,
+ color: '#444',
+ },
+ {
+ display: 'table-cell',
+ flexGrow: 1,
+ }
+);
+
+const RangeLabel = styled.span({
+ paddingLeft: 5,
+ paddingRight: 5,
+ fontSize: 12,
+ whiteSpace: 'nowrap',
+});
+
+const RangeWrapper = styled.div({
+ display: 'flex',
+ alignItems: 'center',
+ width: '100%',
+});
+
+export const RangeControl: FC = ({
+ name,
+ value = 50,
+ onChange,
+ min = 0,
+ max = 100,
+ step = 1,
+}) => {
+ const handleChange = (event: ChangeEvent) => {
+ onChange(name, parse(event.target.value));
+ };
+ return (
+
+ {min}
+
+ {`${value} / ${max}`}
+
+ );
+};
diff --git a/lib/components/src/controls/Text.stories.tsx b/lib/components/src/controls/Text.stories.tsx
new file mode 100644
index 000000000000..02d7bb90b1fc
--- /dev/null
+++ b/lib/components/src/controls/Text.stories.tsx
@@ -0,0 +1,17 @@
+import React, { useState } from 'react';
+import { TextControl } from './Text';
+
+export default {
+ title: 'Controls/Text',
+ component: TextControl,
+};
+
+export const Basic = () => {
+ const [value, setValue] = useState('Hello text');
+ return (
+ <>
+ setValue(newVal)} />
+ {value}
+ >
+ );
+};
diff --git a/lib/components/src/controls/Text.tsx b/lib/components/src/controls/Text.tsx
new file mode 100644
index 000000000000..b90e63bd8f4c
--- /dev/null
+++ b/lib/components/src/controls/Text.tsx
@@ -0,0 +1,13 @@
+import React, { FC, ChangeEvent } from 'react';
+
+import { Form } from '../form';
+import { ControlProps, TextValue, TextConfig } from './types';
+
+export type TextProps = ControlProps & TextConfig;
+
+export const TextControl: FC = ({ name, value, onChange }) => {
+ const handleChange = (event: ChangeEvent) => {
+ onChange(name, event.target.value);
+ };
+ return ;
+};
diff --git a/lib/components/src/controls/index.ts b/lib/components/src/controls/index.ts
new file mode 100644
index 000000000000..90e70459b6f9
--- /dev/null
+++ b/lib/components/src/controls/index.ts
@@ -0,0 +1,11 @@
+export * from './types';
+
+export * from './Array';
+export * from './Boolean';
+export * from './Color';
+export * from './Date';
+export * from './Number';
+export * from './options';
+export * from './Object';
+export * from './Range';
+export * from './Text';
diff --git a/lib/components/src/controls/options/Checkbox.tsx b/lib/components/src/controls/options/Checkbox.tsx
new file mode 100644
index 000000000000..44e24b78518c
--- /dev/null
+++ b/lib/components/src/controls/options/Checkbox.tsx
@@ -0,0 +1,77 @@
+import React, { FC, ChangeEvent, useState } from 'react';
+import { styled } from '@storybook/theming';
+import { ControlProps, OptionsMultiSelection, NormalizedOptionsConfig } from '../types';
+
+const CheckboxesWrapper = styled.div<{ isInline: boolean }>(({ isInline }) =>
+ isInline
+ ? {
+ display: 'flex',
+ flexWrap: 'wrap',
+ alignItems: 'center',
+ '> * + *': {
+ marginLeft: 10,
+ },
+ }
+ : {}
+);
+
+const CheckboxFieldset = styled.fieldset({
+ border: 0,
+ padding: 0,
+ margin: 0,
+});
+
+const CheckboxLabel = styled.label({
+ padding: '3px 0 3px 5px',
+ lineHeight: '18px',
+ display: 'inline-block',
+});
+
+type CheckboxConfig = NormalizedOptionsConfig & { isInline: boolean };
+type CheckboxProps = ControlProps & CheckboxConfig;
+export const CheckboxControl: FC = ({
+ name,
+ options,
+ value,
+ onChange,
+ isInline,
+}) => {
+ const [selected, setSelected] = useState(value || []);
+
+ const handleChange = (e: ChangeEvent) => {
+ const option = (e.target as HTMLInputElement).value;
+ const newVal = [...selected];
+ if (newVal.includes(option)) {
+ newVal.splice(newVal.indexOf(option), 1);
+ } else {
+ newVal.push(option);
+ }
+ onChange(name, newVal);
+ setSelected(newVal);
+ };
+
+ return (
+
+
+ {Object.keys(options).map((key: string) => {
+ const id = `${name}-${key}`;
+ const optionValue = options[key];
+
+ return (
+
+
+ {key}
+
+ );
+ })}
+
+
+ );
+};
diff --git a/lib/components/src/controls/options/Options.stories.tsx b/lib/components/src/controls/options/Options.stories.tsx
new file mode 100644
index 000000000000..d8ce20c14bb8
--- /dev/null
+++ b/lib/components/src/controls/options/Options.stories.tsx
@@ -0,0 +1,54 @@
+import React, { useState } from 'react';
+import { OptionsControl } from './Options';
+
+export default {
+ title: 'Controls/Options',
+ component: OptionsControl,
+};
+
+const arrayOptions = ['Bat', 'Cat', 'Rat'];
+const objectOptions = {
+ A: { id: 'Aardvark' },
+ B: { id: 'Bat' },
+ C: { id: 'Cat' },
+};
+const emptyOptions = null;
+
+const optionsHelper = (options, type) => {
+ const [value, setValue] = useState([]);
+ return (
+ <>
+ setValue(newVal)}
+ />
+ {value && Array.isArray(value) ? (
+ // eslint-disable-next-line react/no-array-index-key
+ {value && value.map((item, idx) => {JSON.stringify(item)} )}
+ ) : (
+ {value ? JSON.stringify(value) : '-'}
+ )}
+ >
+ );
+};
+
+// Check
+export const CheckArray = () => optionsHelper(arrayOptions, 'check');
+export const InlineCheckArray = () => optionsHelper(arrayOptions, 'inline-check');
+export const CheckObject = () => optionsHelper(objectOptions, 'check');
+export const InlineCheckObject = () => optionsHelper(objectOptions, 'inline-check');
+
+// Radio
+export const ArrayRadio = () => optionsHelper(arrayOptions, 'radio');
+export const ArrayInlineRadio = () => optionsHelper(arrayOptions, 'inline-radio');
+export const ObjectRadio = () => optionsHelper(objectOptions, 'radio');
+export const ObjectInlineRadio = () => optionsHelper(objectOptions, 'inline-radio');
+
+// Select
+export const ArraySelect = () => optionsHelper(arrayOptions, 'select');
+export const ArrayMultiSelect = () => optionsHelper(arrayOptions, 'multi-select');
+export const ObjectSelect = () => optionsHelper(objectOptions, 'select');
+export const ObjectMultiSelect = () => optionsHelper(objectOptions, 'multi-select');
diff --git a/lib/components/src/controls/options/Options.tsx b/lib/components/src/controls/options/Options.tsx
new file mode 100644
index 000000000000..37428156afa6
--- /dev/null
+++ b/lib/components/src/controls/options/Options.tsx
@@ -0,0 +1,35 @@
+import React, { FC } from 'react';
+
+import { CheckboxControl } from './Checkbox';
+import { RadioControl } from './Radio';
+import { SelectControl } from './Select';
+import { ControlProps, OptionsSelection, OptionsConfig, Options } from '../types';
+
+const normalizeOptions = (options: Options) => {
+ if (Array.isArray(options)) {
+ return options.reduce((acc, item) => {
+ acc[item] = item;
+ return acc;
+ }, {});
+ }
+ return options;
+};
+
+export type OptionsProps = ControlProps & OptionsConfig;
+export const OptionsControl: FC = (props) => {
+ const { controlType, options } = props;
+ const normalized = { ...props, options: normalizeOptions(options) };
+ switch (controlType || 'select') {
+ case 'check':
+ case 'inline-check':
+ return ;
+ case 'radio':
+ case 'inline-radio':
+ return ;
+ case 'select':
+ case 'multi-select':
+ return ;
+ default:
+ throw new Error(`Unknown options type: ${controlType}`);
+ }
+};
diff --git a/lib/components/src/controls/options/Radio.tsx b/lib/components/src/controls/options/Radio.tsx
new file mode 100644
index 000000000000..ff39af32f8c8
--- /dev/null
+++ b/lib/components/src/controls/options/Radio.tsx
@@ -0,0 +1,48 @@
+import React, { FC, Validator } from 'react';
+import { styled } from '@storybook/theming';
+import { ControlProps, OptionsSingleSelection, NormalizedOptionsConfig } from '../types';
+
+const RadiosWrapper = styled.div<{ isInline: boolean }>(({ isInline }) =>
+ isInline
+ ? {
+ display: 'flex',
+ flexWrap: 'wrap',
+ alignItems: 'center',
+ '> * + *': {
+ marginLeft: 10,
+ },
+ }
+ : {}
+);
+
+const RadioLabel = styled.label({
+ padding: '3px 0 3px 5px',
+ lineHeight: '18px',
+ display: 'inline-block',
+});
+
+type RadioConfig = NormalizedOptionsConfig & { isInline: boolean };
+type RadioProps = ControlProps & RadioConfig;
+export const RadioControl: FC = ({ name, options, value, onChange, isInline }) => {
+ return (
+
+ {Object.keys(options).map((key) => {
+ const id = `${name}-${key}`;
+ const optionValue = options[key];
+ return (
+
+ onChange(name, e.target.value)}
+ checked={optionValue === value}
+ />
+ {key}
+
+ );
+ })}
+
+ );
+};
diff --git a/lib/components/src/controls/options/Select.tsx b/lib/components/src/controls/options/Select.tsx
new file mode 100644
index 000000000000..8709a0c6ad05
--- /dev/null
+++ b/lib/components/src/controls/options/Select.tsx
@@ -0,0 +1,41 @@
+import React, { FC } from 'react';
+import ReactSelect from 'react-select';
+import { styled } from '@storybook/theming';
+import { ControlProps, OptionsSelection, NormalizedOptionsConfig } from '../types';
+
+// TODO: Apply the Storybook theme to react-select
+const OptionsSelect = styled(ReactSelect)({
+ width: '100%',
+ maxWidth: '300px',
+ color: 'black',
+});
+
+interface OptionsItem {
+ value: any;
+ label: string;
+}
+type ReactSelectOnChangeFn = { (v: OptionsItem): void } | { (v: OptionsItem[]): void };
+
+type SelectConfig = NormalizedOptionsConfig & { isMulti: boolean };
+type SelectProps = ControlProps & SelectConfig;
+export const SelectControl: FC = ({ name, value, options, onChange, isMulti }) => {
+ // const optionsIndex = options.findIndex(i => i.value === value);
+ // let defaultValue: typeof options | typeof options[0] = options[optionsIndex];
+ const selectOptions = Object.entries(options).reduce((acc, [key, val]) => {
+ acc.push({ label: key, value: val });
+ return acc;
+ }, []);
+
+ const handleChange: ReactSelectOnChangeFn = isMulti
+ ? (values: OptionsItem[]) => onChange(name, values && values.map((item) => item.value))
+ : (e: OptionsItem) => onChange(name, e.value);
+
+ return (
+
+ );
+};
diff --git a/lib/components/src/controls/options/index.ts b/lib/components/src/controls/options/index.ts
new file mode 100644
index 000000000000..6e8a17140bbf
--- /dev/null
+++ b/lib/components/src/controls/options/index.ts
@@ -0,0 +1 @@
+export * from './Options';
diff --git a/lib/components/src/controls/types.ts b/lib/components/src/controls/types.ts
new file mode 100644
index 000000000000..9d6253b03d48
--- /dev/null
+++ b/lib/components/src/controls/types.ts
@@ -0,0 +1,84 @@
+/* eslint-disable @typescript-eslint/no-empty-interface */
+export interface ControlProps {
+ name: string;
+ value: T;
+ defaultValue?: T;
+ onChange: (name: string, value: T) => T | void;
+}
+
+export type ArrayValue = string[] | readonly string[];
+export interface ArrayConfig {
+ separator?: string;
+}
+
+export type BooleanValue = boolean;
+export interface BooleanConfig {}
+
+export type ColorValue = string;
+export interface ColorConfig {}
+
+export type DateValue = Date | number;
+export interface DateConfig {}
+
+export type NumberValue = number;
+export interface NumberConfig {
+ min?: number;
+ max?: number;
+ step?: number;
+}
+
+export type RangeConfig = NumberConfig;
+
+export type ObjectValue = any;
+export interface ObjectConfig {}
+
+export type OptionsSingleSelection = any;
+export type OptionsMultiSelection = any[];
+export type OptionsSelection = OptionsSingleSelection | OptionsMultiSelection;
+export type OptionsArray = any[];
+export type OptionsObject = Record;
+export type Options = OptionsArray | OptionsObject;
+export type OptionsControlType =
+ | 'radio'
+ | 'inline-radio'
+ | 'check'
+ | 'inline-check'
+ | 'select'
+ | 'multi-select';
+
+export type OptionsConfig = {
+ options: Options;
+ controlType: OptionsControlType;
+};
+
+export type NormalizedOptionsConfig = {
+ options: OptionsObject;
+ controlType: OptionsControlType;
+};
+
+export type TextValue = string;
+export interface TextConfig {}
+
+export type ControlType =
+ | 'array'
+ | 'boolean'
+ | 'color'
+ | 'date'
+ | 'number'
+ | 'range'
+ | 'object'
+ | OptionsControlType
+ | 'text';
+
+export type Control =
+ | ArrayConfig
+ | BooleanConfig
+ | ColorConfig
+ | DateConfig
+ | NumberConfig
+ | ObjectConfig
+ | OptionsConfig
+ | RangeConfig
+ | TextConfig;
+
+export type Controls = Record;
diff --git a/lib/components/src/index.ts b/lib/components/src/index.ts
index 42bd811e7e02..3e088e977270 100644
--- a/lib/components/src/index.ts
+++ b/lib/components/src/index.ts
@@ -40,6 +40,7 @@ export { StorybookIcon } from './brand/StorybookIcon';
// Doc blocks
export * from './blocks';
+export * from './controls';
// Loader
export { Loader } from './Loader/Loader';