Merge branch 'next' into setup-travis

# Conflicts:
#	yarn.lock
This commit is contained in:
Norbert de Langen 2019-07-10 11:58:46 +02:00
commit 6bcdbf209a
437 changed files with 7426 additions and 35351 deletions

View File

@ -1,3 +1,69 @@
## 5.2.0-alpha.39 (July 10, 2019)
### Bug Fixes
* UI: Fix Sidebar input refresh on 'Enter' ([#7342](https://github.com/storybookjs/storybook/pull/7342))
* Addon-knobs: Fix select options types to allow string[] and null ([#7356](https://github.com/storybookjs/storybook/pull/7356))
### Maintenance
* Typescript: Migrate @storybook/react ([#7054](https://github.com/storybookjs/storybook/pull/7054))
* Build: delete tests & snapshots from dist ([#7358](https://github.com/storybookjs/storybook/pull/7358))
## 5.2.0-alpha.38 (July 9, 2019)
### Bug Fixes
* Addon-storysource: Replace loader with source-loader ([#7272](https://github.com/storybookjs/storybook/pull/7272))
### Maintenance
* Typescript: Migrate @storybook/addon-knobs ([#7180](https://github.com/storybookjs/storybook/pull/7180))
### Dependency Upgrades
* Upgrade all dependencies ([#7329](https://github.com/storybookjs/storybook/pull/7329))
## 5.2.0-alpha.37 (July 8, 2019)
### Bug Fixes
* Addon-docs: Use storyFn instead of getDecorated ([#7334](https://github.com/storybookjs/storybook/pull/7334))
* Addon-knobs: Prevent rerender when a button callback returns false. ([#7197](https://github.com/storybookjs/storybook/pull/7197))
* Addons: Fix null parameters in disable addons tab logic ([#7333](https://github.com/storybookjs/storybook/pull/7333))
* Addon-docs: Fix renaming stories on module / MDX format ([#7319](https://github.com/storybookjs/storybook/pull/7319))
* Addon-centered/contexts: Move optionalDependencies to peerDependencies ([#7315](https://github.com/storybookjs/storybook/pull/7315))
### Maintenance
* Typescript: migrate client api ([#7147](https://github.com/storybookjs/storybook/pull/7147))
* Angular-cli: Add addon-docs example ([#7257](https://github.com/storybookjs/storybook/pull/7257))
## 5.2.0-alpha.36 (July 5, 2019)
### Features
* Addon-docs: Added inline option to Story block ([#7308](https://github.com/storybookjs/storybook/pull/7308))
* Addon-knobs: Ensure unique knob names across groups ([#6793](https://github.com/storybookjs/storybook/pull/6793))
* Core: Enable webpack to rebuild changes in node_modules ([#6265](https://github.com/storybooorybook/pull/6265))
* Addons: Disable option for addon tab ([#6923](https://github.com/storybookjs/storybook/pull/6923))
### Bug Fixes
* Fix lint error from #6923 ([#7311](https://github.com/storybookjs/storybook/pull/7311))
* Addon-actions: fix serialization performance ([#7256](https://github.com/storybookjs/storybook/pull/7256))
### Maintenance
* Typescript: Migrate @storybook/addon-event ([#7190](https://github.com/storybookjs/storybook/pull/7190))
* Typescript: Improve actions type ([#7012](https://github.com/storybookjs/storybook/pull/7012))
## 5.2.0-alpha.35 (July 3, 2019)
### Bug Fixes
* React-Native: Fix null story check ([#7243](https://github.com/storybookjs/storybook/pull/7243))
## 5.2.0-alpha.34 (July 2, 2019)
### Bug Fixes
@ -418,7 +484,7 @@ Publish failed
- Addon-docs: Docs page bugfix
- Addon-docs: Fix source block for legacy stories
NOTE: use `@storybook/addon-storysource/loader` with option `injectParameters: true` for legacy source
NOTE: use `@storybook/source-loader` with option `injectParameters: true` for legacy source
## 5.2.0-alpha.6 (May 14, 2019)

View File

@ -1,62 +1,66 @@
# Migration
- [From version 5.1.x to 5.2.x](#from-version-51x-to-52x)
- [Docs mode docgen](#docs-mode-docgen)
- [From version 5.0.x to 5.1.x](#from-version-50x-to-51x)
- [React native server](#react-native-server)
- [Angular 7](#angular-7)
- [CoreJS 3](#corejs-3)
- [From version 5.0.1 to 5.0.2](#from-version-501-to-502)
- [Deprecate webpack extend mode](#deprecate-webpack-extend-mode)
- [From version 4.1.x to 5.0.x](#from-version-41x-to-50x)
- [Webpack config simplification](#webpack-config-simplification)
- [Theming overhaul](#theming-overhaul)
- [Story hierarchy defaults](#story-hierarchy-defaults)
- [Options addon deprecated](#options-addon-deprecated)
- [Individual story decorators](#individual-story-decorators)
- [Addon backgrounds uses parameters](#addon-backgrounds-uses-parameters)
- [Addon cssresources name attribute renamed](#addon-cssresources-name-attribute-renamed)
- [Addon viewport uses parameters](#addon-viewport-uses-parameters)
- [Addon a11y uses parameters](#addon-a11y-uses-parameters-decorator-renamed)
- [New keyboard shortcuts defaults](#new-keyboard-shortcuts-defaults)
- [New URL structure](#new-url-structure)
- [Vue integration](#vue-integration)
- [From version 4.0.x to 4.1.x](#from-version-40x-to-41x)
- [Private addon config](#private-addon-config)
- [React 15.x](#react-15x)
- [From version 3.4.x to 4.0.x](#from-version-34x-to-40x)
- [React 16.3+](#react-163)
- [Generic addons](#generic-addons)
- [Knobs select ordering](#knobs-select-ordering)
- [Knobs URL parameters](#knobs-url-parameters)
- [Keyboard shortcuts moved](#keyboard-shortcuts-moved)
- [Removed addWithInfo](#removed-addwithinfo)
- [Removed RN packager](#removed-rn-packager)
- [Removed RN addons](#removed-rn-addons)
- [Storyshots Changes](#storyshots-changes)
- [Webpack 4](#webpack-4)
- [Babel 7](#babel-7)
- [Create-react-app](#create-react-app)
- [Upgrade CRA1 to babel 7](#upgrade-cra1-to-babel-7)
- [Migrate CRA1 while keeping babel 6](#migrate-cra1-while-keeping-babel-6)
- [start-storybook opens browser](#start-storybook-opens-browser)
- [CLI Rename](#cli-rename)
- [Addon story parameters](#addon-story-parameters)
- [From version 3.3.x to 3.4.x](#from-version-33x-to-34x)
- [From version 3.2.x to 3.3.x](#from-version-32x-to-33x)
- [`babel-core` is now a peer dependency (#2494)](#babel-core-is-now-a-peer-dependency-2494)
- [Base webpack config now contains vital plugins (#1775)](#base-webpack-config-now-contains-vital-plugins-1775)
- [Refactored Knobs](#refactored-knobs)
- [From version 3.1.x to 3.2.x](#from-version-31x-to-32x)
- [Moved TypeScript addons definitions](#moved-typescript-addons-definitions)
- [Updated Addons API](#updated-addons-api)
- [From version 3.0.x to 3.1.x](#from-version-30x-to-31x)
- [Moved TypeScript definitions](#moved-typescript-definitions)
- [Deprecated head.html](#deprecated-headhtml)
- [From version 2.x.x to 3.x.x](#from-version-2xx-to-3xx)
- [Webpack upgrade](#webpack-upgrade)
- [Packages renaming](#packages-renaming)
- [Deprecated embedded addons](#deprecated-embedded-addons)
- [Migration](#Migration)
- [From version 5.1.x to 5.2.x](#From-version-51x-to-52x)
- [Docs mode docgen](#Docs-mode-docgen)
- [storySort option](#storySort-option)
- [From version 5.0.x to 5.1.x](#From-version-50x-to-51x)
- [React native server](#React-native-server)
- [Angular 7](#Angular-7)
- [CoreJS 3](#CoreJS-3)
- [From version 5.0.1 to 5.0.2](#From-version-501-to-502)
- [Deprecate webpack extend mode](#Deprecate-webpack-extend-mode)
- [From version 4.1.x to 5.0.x](#From-version-41x-to-50x)
- [sortStoriesByKind](#sortStoriesByKind)
- [Webpack config simplification](#Webpack-config-simplification)
- [Theming overhaul](#Theming-overhaul)
- [Story hierarchy defaults](#Story-hierarchy-defaults)
- [Options addon deprecated](#Options-addon-deprecated)
- [Individual story decorators](#Individual-story-decorators)
- [Addon backgrounds uses parameters](#Addon-backgrounds-uses-parameters)
- [Addon cssresources name attribute renamed](#Addon-cssresources-name-attribute-renamed)
- [Addon viewport uses parameters](#Addon-viewport-uses-parameters)
- [Addon a11y uses parameters, decorator renamed](#Addon-a11y-uses-parameters-decorator-renamed)
- [New keyboard shortcuts defaults](#New-keyboard-shortcuts-defaults)
- [New URL structure](#New-URL-structure)
- [Rename of the `--secure` cli parameter to `--https`](#Rename-of-the---secure-cli-parameter-to---https)
- [Vue integration](#Vue-integration)
- [From version 4.0.x to 4.1.x](#From-version-40x-to-41x)
- [Private addon config](#Private-addon-config)
- [React 15.x](#React-15x)
- [From version 3.4.x to 4.0.x](#From-version-34x-to-40x)
- [React 16.3+](#React-163)
- [Generic addons](#Generic-addons)
- [Knobs select ordering](#Knobs-select-ordering)
- [Knobs URL parameters](#Knobs-URL-parameters)
- [Keyboard shortcuts moved](#Keyboard-shortcuts-moved)
- [Removed addWithInfo](#Removed-addWithInfo)
- [Removed RN packager](#Removed-RN-packager)
- [Removed RN addons](#Removed-RN-addons)
- [Storyshots Changes](#Storyshots-Changes)
- [Webpack 4](#Webpack-4)
- [Babel 7](#Babel-7)
- [Create-react-app](#Create-react-app)
- [Upgrade CRA1 to babel 7](#Upgrade-CRA1-to-babel-7)
- [Migrate CRA1 while keeping babel 6](#Migrate-CRA1-while-keeping-babel-6)
- [start-storybook opens browser](#start-storybook-opens-browser)
- [CLI Rename](#CLI-Rename)
- [Addon story parameters](#Addon-story-parameters)
- [From version 3.3.x to 3.4.x](#From-version-33x-to-34x)
- [From version 3.2.x to 3.3.x](#From-version-32x-to-33x)
- [`babel-core` is now a peer dependency (#2494)](#babel-core-is-now-a-peer-dependency-2494)
- [Base webpack config now contains vital plugins (#1775)](#Base-webpack-config-now-contains-vital-plugins-1775)
- [Refactored Knobs](#Refactored-Knobs)
- [From version 3.1.x to 3.2.x](#From-version-31x-to-32x)
- [Moved TypeScript addons definitions](#Moved-TypeScript-addons-definitions)
- [Updated Addons API](#Updated-Addons-API)
- [From version 3.0.x to 3.1.x](#From-version-30x-to-31x)
- [Moved TypeScript definitions](#Moved-TypeScript-definitions)
- [Deprecated head.html](#Deprecated-headhtml)
- [From version 2.x.x to 3.x.x](#From-version-2xx-to-3xx)
- [Webpack upgrade](#Webpack-upgrade)
- [Packages renaming](#Packages-renaming)
- [Deprecated embedded addons](#Deprecated-embedded-addons)
## From version 5.1.x to 5.2.x
@ -67,6 +71,20 @@ This isn't a breaking change per se, because `addon-docs` is a new feature. Howe
1. Support for only one prop table
2. Prop table docgen info should be stored on the component and not in the global variable `STORYBOOK_REACT_CLASSES` as before.
### storySort option
In 5.0.x the global option `sortStoriesByKind` option was [inadverttly removed](#sortstoriesbykind). In 5.2 we've introduced a new option, `storySort`, to replace it. `storySort` takes a comparator function, so it is strictly more powerful than `sortStoriesByKind`.
For example, here's how to sort by story ID using `storySort`:
```js
addParameters({
options: {
storySort: (a, b) => a[1].id.localeCompare(b[1].id),
},
});
```
## From version 5.0.x to 5.1.x
### React native server

View File

@ -163,7 +163,7 @@ Looking for a first issue to tackle?
### Development scripts
Storybook is organized as a monorepo using [Lerna](https://lernajs.io). Useful scripts include:
Storybook is organized as a monorepo using [Lerna](https://lerna.js.org/). Useful scripts include:
#### `yarn bootstrap`

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/addon-a11y",
"version": "5.2.0-alpha.34",
"version": "5.2.0-alpha.39",
"description": "a11y addon for storybook",
"keywords": [
"a11y",
@ -26,12 +26,12 @@
"prepare": "node ../../scripts/prepare.js"
},
"dependencies": {
"@storybook/addons": "5.2.0-alpha.34",
"@storybook/api": "5.2.0-alpha.34",
"@storybook/client-logger": "5.2.0-alpha.34",
"@storybook/components": "5.2.0-alpha.34",
"@storybook/core-events": "5.2.0-alpha.34",
"@storybook/theming": "5.2.0-alpha.34",
"@storybook/addons": "5.2.0-alpha.39",
"@storybook/api": "5.2.0-alpha.39",
"@storybook/client-logger": "5.2.0-alpha.39",
"@storybook/components": "5.2.0-alpha.39",
"@storybook/core-events": "5.2.0-alpha.39",
"@storybook/theming": "5.2.0-alpha.39",
"axe-core": "^3.2.2",
"common-tags": "^1.8.0",
"core-js": "^3.0.1",

View File

@ -34,15 +34,15 @@ const Icon = styled(Icons)(
: {}
);
const Passes = styled.span(({ theme }) => ({
const Passes = styled.span<{}>(({ theme }) => ({
color: theme.color.positive,
}));
const Violations = styled.span(({ theme }) => ({
const Violations = styled.span<{}>(({ theme }) => ({
color: theme.color.negative,
}));
const Incomplete = styled.span(({ theme }) => ({
const Incomplete = styled.span<{}>(({ theme }) => ({
color: theme.color.warning,
}));

View File

@ -11,7 +11,7 @@ const Item = styled.li({
fontWeight: 600,
});
const ItemTitle = styled.span(({ theme }) => ({
const ItemTitle = styled.span<{}>(({ theme }) => ({
borderBottom: `1px solid ${theme.appBorderColor}`,
width: '100%',
display: 'flex',

View File

@ -31,7 +31,7 @@ enum CheckBoxStates {
INDETERMINATE,
}
const Checkbox = styled.input(({ disabled }) => ({
const Checkbox = styled.input<{ disabled: boolean }>(({ disabled }) => ({
cursor: disabled ? 'not-allowed' : 'pointer',
}));

View File

@ -10,7 +10,7 @@ import { Tags } from './Tags';
import { RuleType } from '../A11YPanel';
import HighlightToggle from './HighlightToggle';
const Wrapper = styled.div(({ theme }) => ({
const Wrapper = styled.div<{}>(({ theme }) => ({
display: 'flex',
width: '100%',
borderBottom: `1px solid ${theme.appBorderColor}`,
@ -30,7 +30,7 @@ const Icon = styled<any, any>(Icons)(({ theme }) => ({
display: 'inline-flex',
}));
const HeaderBar = styled.div(({ theme }) => ({
const HeaderBar = styled.div<{}>(({ theme }) => ({
padding: theme.layoutMargin,
paddingLeft: theme.layoutMargin - 3,
background: 'none',

View File

@ -9,7 +9,7 @@ const Wrapper = styled.div({
margin: '12px 0',
});
const Item = styled.div(({ theme }) => ({
const Item = styled.div<{}>(({ theme }) => ({
margin: '0 6px',
padding: '5px',
border: `1px solid ${theme.appBorderColor}`,

View File

@ -321,7 +321,7 @@ exports[`HighlightToggle component should match snapshot 1`] = `
}
}
>
<ConnectFunction>
<Connect(HighlightToggle)>
<HighlightToggle
addElement={[Function]}
elementsToHighlight={Array []}
@ -346,7 +346,7 @@ exports[`HighlightToggle component should match snapshot 1`] = `
/>
</Styled(input)>
</HighlightToggle>
</ConnectFunction>
</Connect(HighlightToggle)>
</ThemeProvider>
</ThemedHighlightToggle>
</Provider>

View File

@ -15,7 +15,7 @@ const Container = styled.div({
minHeight: '100%',
});
const HighlightToggleLabel = styled.label(({ theme }) => ({
const HighlightToggleLabel = styled.label<{}>(({ theme }) => ({
cursor: 'pointer',
userSelect: 'none',
marginBottom: '3px',
@ -77,7 +77,7 @@ const Item = styled.button(
const TabsWrapper = styled.div({});
const List = styled.div(({ theme }) => ({
const List = styled.div<{}>(({ theme }) => ({
boxShadow: `${theme.appBorderColor} 0 -1px 0 0 inset`,
background: 'rgba(0, 0, 0, .05)',
display: 'flex',

View File

@ -2,7 +2,7 @@ import React, { Fragment, FunctionComponent } from 'react';
import { styled } from '@storybook/theming';
import { addons, types } from '@storybook/addons';
import { ADDON_ID, PANEL_ID } from './constants';
import { ADDON_ID, PANEL_ID, PARAM_KEY } from './constants';
import { ColorBlindness } from './components/ColorBlindness';
import { A11YPanel } from './components/A11YPanel';
@ -94,6 +94,7 @@ addons.register(ADDON_ID, api => {
title: 'Accessibility',
type: types.PANEL,
render: ({ active, key }) => <A11YPanel key={key} api={api} active={active} />,
paramKey: PARAM_KEY,
});
addons.add(PANEL_ID, {

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/addon-actions",
"version": "5.2.0-alpha.34",
"version": "5.2.0-alpha.39",
"description": "Action Logger addon for storybook",
"keywords": [
"storybook"
@ -21,11 +21,11 @@
"prepare": "node ../../scripts/prepare.js"
},
"dependencies": {
"@storybook/addons": "5.2.0-alpha.34",
"@storybook/api": "5.2.0-alpha.34",
"@storybook/components": "5.2.0-alpha.34",
"@storybook/core-events": "5.2.0-alpha.34",
"@storybook/theming": "5.2.0-alpha.34",
"@storybook/addons": "5.2.0-alpha.39",
"@storybook/api": "5.2.0-alpha.39",
"@storybook/components": "5.2.0-alpha.39",
"@storybook/core-events": "5.2.0-alpha.39",
"@storybook/theming": "5.2.0-alpha.39",
"core-js": "^3.0.1",
"fast-deep-equal": "^2.0.1",
"global": "^4.3.2",

View File

@ -10,7 +10,7 @@ export const Action = styled.div({
alignItems: 'flex-start',
});
export const Counter = styled.div(({ theme }) => ({
export const Counter = styled.div<{}>(({ theme }) => ({
backgroundColor: opacify(0.5, theme.appBorderColor),
color: theme.color.inverseText,
fontSize: theme.typography.size.s1,

View File

@ -1,3 +1,4 @@
export const PARAM_KEY = 'actions';
export const ADDON_ID = 'storybook/actions';
export const PANEL_ID = `${ADDON_ID}/panel`;
export const EVENT_ID = `${ADDON_ID}/action-event`;

View File

@ -1,13 +1,14 @@
import React from 'react';
import addons from '@storybook/addons';
import ActionLogger from './containers/ActionLogger';
import { ADDON_ID, PANEL_ID } from '.';
import { ADDON_ID, PANEL_ID, PARAM_KEY } from './constants';
export function register() {
addons.register(ADDON_ID, api => {
addons.addPanel(PANEL_ID, {
title: 'Actions',
render: ({ active, key }) => <ActionLogger key={key} api={api} active={active} />,
paramKey: PARAM_KEY,
});
});
}

View File

@ -2,4 +2,5 @@ export interface ActionOptions {
depth?: number;
clearOnStoryChange?: boolean;
limit?: number;
allowFunction?: boolean;
}

View File

@ -0,0 +1,81 @@
import { ActionOptions } from './ActionOptions';
import { ActionsMap } from './ActionsMap';
export interface ActionsFunction {
<T extends string>(handlerMap: Record<T, string>, options?: ActionOptions): ActionsMap<T>;
<T extends string>(...handlers: T[]): ActionsMap<T>;
<T extends string>(handler1: T, options?: ActionOptions): ActionsMap<T>;
<T extends string>(handler1: T, handler2: T, options?: ActionOptions): ActionsMap<T>;
<T extends string>(handler1: T, handler2: T, handler3: T, options?: ActionOptions): ActionsMap<T>;
<T extends string>(
handler1: T,
handler2: T,
handler3: T,
handler4: T,
options?: ActionOptions
): ActionsMap<T>;
<T extends string>(
handler1: T,
handler2: T,
handler3: T,
handler4: T,
handler5: T,
options?: ActionOptions
): ActionsMap<T>;
<T extends string>(
handler1: T,
handler2: T,
handler3: T,
handler4: T,
handler5: T,
handler6: T,
options?: ActionOptions
): ActionsMap<T>;
<T extends string>(
handler1: T,
handler2: T,
handler3: T,
handler4: T,
handler5: T,
handler6: T,
handler7: T,
options?: ActionOptions
): ActionsMap<T>;
<T extends string>(
handler1: T,
handler2: T,
handler3: T,
handler4: T,
handler5: T,
handler6: T,
handler7: T,
handler8: T,
options?: ActionOptions
): ActionsMap<T>;
<T extends string>(
handler1: T,
handler2: T,
handler3: T,
handler4: T,
handler5: T,
handler6: T,
handler7: T,
handler8: T,
handler9: T,
options?: ActionOptions
): ActionsMap<T>;
<T extends string>(
handler1: T,
handler2: T,
handler3: T,
handler4: T,
handler5: T,
handler6: T,
handler7: T,
handler8: T,
handler9: T,
handler10: T,
options?: ActionOptions
): ActionsMap<T>;
}

View File

@ -1,5 +1,3 @@
import { HandlerFunction } from './HandlerFunction';
export interface ActionsMap {
[key: string]: HandlerFunction;
}
export type ActionsMap<T extends string = string> = Record<T, HandlerFunction>;

View File

@ -1,4 +1,5 @@
export * from './ActionDisplay';
export * from './ActionsFunction';
export * from './ActionOptions';
export * from './ActionsMap';
export * from './DecoratorFunction';

View File

@ -91,3 +91,62 @@ describe('Depth config', () => {
});
});
});
describe('allowFunction config', () => {
it('with global allowFunction false', () => {
const channel = createChannel();
const allowFunction = false;
configureActions({
allowFunction,
});
action('test-action')({
root: {
one: {
a: 1,
b: () => 'foo',
},
},
});
expect(getChannelData(channel)[0]).toEqual({
root: {
one: {
a: 1,
b: expect.any(Function),
},
},
});
});
// TODO: this test is pretty pointless, as the real channel isn't used, nothing is changed
it('with global allowFunction true', () => {
const channel = createChannel();
const allowFunction = true;
configureActions({
allowFunction,
});
action('test-action')({
root: {
one: {
a: 1,
b: () => 'foo',
},
},
});
expect(getChannelData(channel)[0]).toEqual({
root: {
one: {
a: 1,
b: expect.any(Function),
},
},
});
});
});

View File

@ -22,6 +22,7 @@ export function action(name: string, options: ActionOptions = {}): HandlerFuncti
options: {
...actionOptions,
depth: minDepth + (actionOptions.depth || 3),
allowFunction: actionOptions.allowFunction || false,
},
};
channel.emit(EVENT_ID, actionDisplayToEmit);

View File

@ -1,8 +1,8 @@
import { action } from './action';
import { ActionOptions, ActionsMap } from '../models';
import { ActionsFunction, ActionOptions, ActionsMap } from '../models';
import { config } from './configureActions';
export function actions(...args: any[]): ActionsMap {
export const actions: ActionsFunction = (...args: any[]) => {
let options: ActionOptions = config;
const names = args;
// last argument can be options
@ -26,4 +26,4 @@ export function actions(...args: any[]): ActionsMap {
actionsObject[name] = action(namesObject[name], options);
});
return actionsObject;
}
};

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/addon-backgrounds",
"version": "5.2.0-alpha.34",
"version": "5.2.0-alpha.39",
"description": "A storybook addon to show different backgrounds for your preview",
"keywords": [
"addon",
@ -25,12 +25,12 @@
"prepare": "node ../../scripts/prepare.js"
},
"dependencies": {
"@storybook/addons": "5.2.0-alpha.34",
"@storybook/api": "5.2.0-alpha.34",
"@storybook/client-logger": "5.2.0-alpha.34",
"@storybook/components": "5.2.0-alpha.34",
"@storybook/core-events": "5.2.0-alpha.34",
"@storybook/theming": "5.2.0-alpha.34",
"@storybook/addons": "5.2.0-alpha.39",
"@storybook/api": "5.2.0-alpha.39",
"@storybook/client-logger": "5.2.0-alpha.39",
"@storybook/components": "5.2.0-alpha.39",
"@storybook/core-events": "5.2.0-alpha.39",
"@storybook/theming": "5.2.0-alpha.39",
"core-js": "^3.0.1",
"memoizerific": "^1.11.3",
"react": "^16.8.3",

View File

@ -1,3 +1,5 @@
import { StoryFn } from "@storybook/addons";
export interface ICollection {
[p: string]: any;
}
@ -11,11 +13,13 @@ export interface NgModuleMetadata {
}
export interface IStory {
props?: ICollection;
moduleMetadata?: Partial<NgModuleMetadata>;
component?: any;
props?: ICollection;
propsMeta?: ICollection;
moduleMetadata?: NgModuleMetadata;
template?: string;
styles?: string[];
}
declare module '@storybook/addon-centered/angular' {
export function centered(story: IStory): IStory;
export function centered(story: StoryFn<IStory>): IStory;
}

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/addon-centered",
"version": "5.2.0-alpha.34",
"version": "5.2.0-alpha.39",
"description": "Storybook decorator to center components",
"keywords": [
"addon",
@ -23,14 +23,18 @@
"prepare": "node ../../scripts/prepare.js"
},
"dependencies": {
"@storybook/addons": "5.2.0-alpha.39",
"core-js": "^3.0.1",
"global": "^4.3.2",
"util-deprecate": "^1.0.2"
},
"devDependencies": {
"@types/mithril": "^1.1.16"
"@types/mithril": "^1.1.16",
"mithril": "*",
"preact": "*",
"react": "*"
},
"optionalDependencies": {
"peerDependencies": {
"mithril": "*",
"preact": "*",
"react": "*"

View File

@ -1,3 +1,5 @@
import { StoryFn } from '@storybook/addons';
import { IStory } from '../angular.d';
import styles from './styles';
function getComponentSelector(component: any) {
@ -43,7 +45,7 @@ function getModuleMetadata(metadata: any) {
return moduleMetadata;
}
export default function(metadataFn: any) {
export default function(metadataFn: StoryFn<IStory>) {
const metadata = metadataFn();
return {

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/addon-contexts",
"version": "5.2.0-alpha.34",
"version": "5.2.0-alpha.39",
"description": "Storybook Addon Contexts",
"keywords": [
"storybook",
@ -28,17 +28,20 @@
"dev:check-types": "tsc --noEmit"
},
"dependencies": {
"@storybook/addons": "5.2.0-alpha.34",
"@storybook/api": "5.2.0-alpha.34",
"@storybook/components": "5.2.0-alpha.34",
"@storybook/core-events": "5.2.0-alpha.34",
"@storybook/addons": "5.2.0-alpha.39",
"@storybook/api": "5.2.0-alpha.39",
"@storybook/components": "5.2.0-alpha.39",
"@storybook/core-events": "5.2.0-alpha.39",
"core-js": "^3.0.1"
},
"peerDependencies": {
"global": "*",
"qs": "*"
"preact": "*",
"qs": "*",
"react": "*",
"vue": "*"
},
"optionalDependencies": {
"devDependencies": {
"preact": "*",
"react": "*",
"vue": "*"

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/addon-cssresources",
"version": "5.2.0-alpha.34",
"version": "5.2.0-alpha.39",
"description": "A storybook addon to switch between css resources at runtime for your story",
"keywords": [
"addon",
@ -25,10 +25,10 @@
"prepare": "node ../../scripts/prepare.js"
},
"dependencies": {
"@storybook/addons": "5.2.0-alpha.34",
"@storybook/api": "5.2.0-alpha.34",
"@storybook/components": "5.2.0-alpha.34",
"@storybook/core-events": "5.2.0-alpha.34",
"@storybook/addons": "5.2.0-alpha.39",
"@storybook/api": "5.2.0-alpha.39",
"@storybook/components": "5.2.0-alpha.39",
"@storybook/core-events": "5.2.0-alpha.39",
"core-js": "^3.0.1",
"global": "^4.3.2",
"react": "^16.8.3"

View File

@ -1,7 +1,7 @@
import React from 'react';
import { addons, types } from '@storybook/addons';
import { ADDON_ID, PANEL_ID } from './constants';
import { ADDON_ID, PANEL_ID, PARAM_KEY } from './constants';
import { CssResourcePanel } from './css-resource-panel';
addons.register(ADDON_ID, api => {
@ -10,5 +10,6 @@ addons.register(ADDON_ID, api => {
type: types.PANEL,
title: 'CSS resources',
render: ({ active }) => <CssResourcePanel key={PANEL_ID} api={api} active={active} />,
paramKey: PARAM_KEY,
});
});

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/addon-design-assets",
"version": "5.2.0-alpha.34",
"version": "5.2.0-alpha.39",
"description": "Design asset preview for storybook",
"keywords": [
"addon",
@ -27,12 +27,12 @@
"prepare": "node ../../scripts/prepare.js"
},
"dependencies": {
"@storybook/addons": "5.2.0-alpha.34",
"@storybook/api": "5.2.0-alpha.34",
"@storybook/client-logger": "5.2.0-alpha.34",
"@storybook/components": "5.2.0-alpha.34",
"@storybook/core-events": "5.2.0-alpha.34",
"@storybook/theming": "5.2.0-alpha.34",
"@storybook/addons": "5.2.0-alpha.39",
"@storybook/api": "5.2.0-alpha.39",
"@storybook/client-logger": "5.2.0-alpha.39",
"@storybook/components": "5.2.0-alpha.39",
"@storybook/core-events": "5.2.0-alpha.39",
"@storybook/theming": "5.2.0-alpha.39",
"common-tags": "^1.8.0",
"core-js": "^3.0.1",
"global": "^4.3.2",

View File

@ -2,7 +2,7 @@ import React from 'react';
import { addons, types } from '@storybook/addons';
import { AddonPanel } from '@storybook/components';
import { ADDON_ID, PANEL_ID } from './constants';
import { ADDON_ID, PANEL_ID, PARAM_KEY } from './constants';
import { Panel } from './panel';
addons.register(ADDON_ID, () => {
@ -14,5 +14,6 @@ addons.register(ADDON_ID, () => {
<Panel />
</AddonPanel>
),
paramKey: PARAM_KEY,
});
});

1
addons/docs/angular/index.js vendored Normal file
View File

@ -0,0 +1 @@
module.exports = require('../common/index');

1
addons/docs/angular/preset.js vendored Normal file
View File

@ -0,0 +1 @@
module.exports = require('../common/preset');

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/addon-docs",
"version": "5.2.0-alpha.34",
"version": "5.2.0-alpha.39",
"description": "Superior documentation for your components",
"keywords": [
"addon",
@ -28,11 +28,11 @@
"@mdx-js/loader": "^1.0.0",
"@mdx-js/mdx": "^1.0.0",
"@mdx-js/react": "^1.0.16",
"@storybook/addons": "5.2.0-alpha.34",
"@storybook/api": "5.2.0-alpha.34",
"@storybook/components": "5.2.0-alpha.34",
"@storybook/router": "5.2.0-alpha.34",
"@storybook/theming": "5.2.0-alpha.34",
"@storybook/addons": "5.2.0-alpha.39",
"@storybook/api": "5.2.0-alpha.39",
"@storybook/components": "5.2.0-alpha.39",
"@storybook/router": "5.2.0-alpha.39",
"@storybook/theming": "5.2.0-alpha.39",
"core-js": "^3.0.1",
"global": "^4.3.2",
"lodash": "^4.17.11",

View File

@ -7,6 +7,7 @@ import { DocsContext, DocsContextProps } from './DocsContext';
interface CommonProps {
height?: string;
inline?: boolean;
}
type StoryDefProps = {
@ -38,15 +39,17 @@ export const getStoryProps = (
const inputId = id === CURRENT_SELECTION ? currentId : id;
const previewId = inputId || toId(mdxKind, name);
const { height } = props;
const { height, inline } = props;
const data = storyStore.fromId(previewId);
const { framework = null } = parameters || {};
// prefer props, then global options, then framework-inferred values
const { inlineStories = inferInlineStories(framework), iframeHeight = undefined } =
(parameters && parameters.options && parameters.options.docs) || {};
return {
inline: inlineStories,
inline: typeof inline === 'boolean' ? inline : inlineStories,
id: previewId,
storyFn: data && data.getDecorated(),
storyFn: data && data.storyFn,
height: height || iframeHeight,
title: data && data.name,
};

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/addon-events",
"version": "5.2.0-alpha.34",
"version": "5.2.0-alpha.39",
"description": "Add events to your Storybook stories.",
"keywords": [
"addon",
@ -19,14 +19,15 @@
},
"license": "MIT",
"main": "dist/index.js",
"jsnext:main": "src/index.js",
"types": "dist/index.d.ts",
"scripts": {
"prepare": "node ../../scripts/prepare.js"
},
"dependencies": {
"@storybook/addons": "5.2.0-alpha.34",
"@storybook/core-events": "5.2.0-alpha.34",
"@storybook/theming": "5.2.0-alpha.34",
"@storybook/addons": "5.2.0-alpha.39",
"@storybook/api": "5.2.0-alpha.39",
"@storybook/core-events": "5.2.0-alpha.39",
"@storybook/theming": "5.2.0-alpha.39",
"core-js": "^3.0.1",
"format-json": "^1.0.3",
"lodash": "^4.17.11",

View File

@ -5,10 +5,19 @@ import isEqual from 'lodash/isEqual';
import { styled } from '@storybook/theming';
import json from 'format-json';
import Textarea from 'react-textarea-autosize';
import { OnEmitEvent } from '../index';
const StyledTextarea = styled(Textarea)(
interface StyledTextareaProps {
shown: boolean;
failed: boolean;
value?: string;
onChange?: (event: React.ChangeEvent<HTMLTextAreaElement>) => void;
}
const StyledTextarea = styled(({ shown, failed, ...rest }: StyledTextareaProps) => (
<Textarea {...rest} />
))<{ shown: boolean; failed: boolean }>(
{
flex: '1 0 0',
boxSizing: 'border-box',
@ -67,7 +76,7 @@ const Label = styled.label({
textAlign: 'right',
width: 100,
fontWeight: '600',
});
} as any);
const Wrapper = styled.div({
display: 'flex',
@ -77,15 +86,29 @@ const Wrapper = styled.div({
width: '100%',
});
function getJSONFromString(str) {
function getJSONFromString(str: string) {
try {
return JSON.parse(str);
} catch (e) {
return str;
}
}
interface ItemProps {
name: string;
title: string;
onEmit: (event: OnEmitEvent) => void;
payload: unknown;
}
class Item extends Component {
interface ItemState {
isTextAreaShowed: boolean;
failed: boolean;
payload: unknown;
payloadString: string;
prevPayload: unknown;
}
class Item extends Component<ItemProps, ItemState> {
static propTypes = {
name: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
@ -98,12 +121,16 @@ class Item extends Component {
payload: {},
};
state = {
state: ItemState = {
isTextAreaShowed: false,
failed: false,
payload: null,
payloadString: '',
prevPayload: null,
};
onChange = ({ target: { value } }) => {
const newState = {
onChange = ({ target: { value } }: React.ChangeEvent<HTMLTextAreaElement>) => {
const newState: Partial<ItemState> = {
payloadString: value,
};
@ -114,7 +141,7 @@ class Item extends Component {
newState.failed = true;
}
this.setState(newState);
this.setState(state => ({ ...state, ...newState }));
};
onEmitClick = () => {
@ -133,7 +160,7 @@ class Item extends Component {
}));
};
static getDerivedStateFromProps = ({ payload }, { prevPayload }) => {
static getDerivedStateFromProps = ({ payload }: ItemProps, { prevPayload }: ItemState) => {
if (!isEqual(payload, prevPayload)) {
const payloadString = json.plain(payload);
const refinedPayload = getJSONFromString(payloadString);
@ -150,7 +177,6 @@ class Item extends Component {
render() {
const { title, name } = this.props;
const { failed, isTextAreaShowed, payloadString } = this.state;
return (
<Wrapper>
<Label htmlFor={`addon-event-${name}`}>{title}</Label>

View File

@ -2,9 +2,11 @@ import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { styled } from '@storybook/theming';
import { API } from '@storybook/api';
import { EVENTS } from '../constants';
import Event from './Event';
import { Event as EventType, OnEmitEvent } from '../index';
const Wrapper = styled.div({
width: '100%',
@ -13,7 +15,15 @@ const Wrapper = styled.div({
minHeight: '100%',
});
export default class EventsPanel extends Component {
interface EventsPanelProps {
active: boolean;
api: API;
}
interface EventsPanelState {
events: EventType[];
}
export default class EventsPanel extends Component<EventsPanelProps, EventsPanelState> {
static propTypes = {
active: PropTypes.bool.isRequired,
api: PropTypes.shape({
@ -23,7 +33,7 @@ export default class EventsPanel extends Component {
}).isRequired,
};
state = {
state: EventsPanelState = {
events: [],
};
@ -39,13 +49,12 @@ export default class EventsPanel extends Component {
api.off(EVENTS.ADD, this.onAdd);
}
onAdd = events => {
onAdd = (events: EventType[]) => {
this.setState({ events });
};
onEmit = event => {
onEmit = (event: OnEmitEvent) => {
const { api } = this.props;
api.emit(EVENTS.EMIT, event);
};

View File

@ -1,3 +1,5 @@
export const PARAM_KEY = 'events';
export const ADDON_ID = 'storybook/events';
export const PANEL_ID = `${ADDON_ID}/panel`;

View File

@ -1,13 +1,20 @@
import { ReactNode } from 'react';
import addons from '@storybook/addons';
import CoreEvents from '@storybook/core-events';
import deprecate from 'util-deprecate';
import { EVENTS } from './constants';
let prevEvents;
let currentEmit;
let prevEvents: Event[];
let currentEmit: (name: string, payload: unknown) => void;
const onEmit = event => {
export interface OnEmitEvent {
name: string;
payload: unknown;
}
const onEmit = (event: OnEmitEvent) => {
currentEmit(event.name, event.payload);
};
@ -21,7 +28,7 @@ const subscription = () => {
};
};
const addEvents = ({ emit, events }) => {
const addEvents = ({ emit, events }: Options) => {
if (prevEvents !== events) {
addons.getChannel().emit(EVENTS.ADD, events);
prevEvents = events;
@ -30,16 +37,28 @@ const addEvents = ({ emit, events }) => {
addons.getChannel().emit(CoreEvents.REGISTER_SUBSCRIPTION, subscription);
};
const WithEvents = deprecate(({ children, ...options }) => {
export interface Event {
name: string;
title: string;
payload: unknown;
}
interface Options {
children?: ReactNode;
emit: (eventName: string, ...args: any) => void;
events: Event[];
}
const WithEvents = deprecate(({ children, ...options }: Options) => {
addEvents(options);
return children;
}, `<WithEvents> usage is deprecated, use .addDecorator(withEvents({emit, events})) instead`);
export default options => {
export default (options: Options) => {
if (options.children) {
return WithEvents(options);
}
return storyFn => {
return (storyFn: () => ReactNode) => {
addEvents(options);
return storyFn();
};

View File

@ -2,14 +2,14 @@ import React from 'react';
import addons from '@storybook/addons';
import Panel from './components/Panel';
import { ADDON_ID, PANEL_ID } from './constants';
import { ADDON_ID, PANEL_ID, PARAM_KEY } from './constants';
export function register() {
addons.register(ADDON_ID, api => {
addons.addPanel(PANEL_ID, {
title: 'Events',
// eslint-disable-next-line react/prop-types
render: ({ active, key }) => <Panel key={key} api={api} active={active} />,
paramKey: PARAM_KEY,
});
});
}

2
addons/events/src/typings.d.ts vendored Normal file
View File

@ -0,0 +1,2 @@
declare module 'react-lifecycles-compat';
declare module 'format-json';

View File

@ -0,0 +1,13 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"types": ["webpack-env"]
},
"include": [
"src/**/*"
],
"exclude": [
"src/__tests__/**/*"
]
}

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/addon-google-analytics",
"version": "5.2.0-alpha.34",
"version": "5.2.0-alpha.39",
"description": "Storybook addon for google analytics",
"keywords": [
"addon",
@ -20,8 +20,8 @@
"prepare": "node ../../scripts/prepare.js"
},
"dependencies": {
"@storybook/addons": "5.2.0-alpha.34",
"@storybook/core-events": "5.2.0-alpha.34",
"@storybook/addons": "5.2.0-alpha.39",
"@storybook/core-events": "5.2.0-alpha.39",
"core-js": "^3.0.1",
"global": "^4.3.2",
"react-ga": "^2.5.7"

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/addon-graphql",
"version": "5.2.0-alpha.34",
"version": "5.2.0-alpha.39",
"description": "Storybook addon to display the GraphiQL IDE",
"keywords": [
"addon",
@ -22,8 +22,8 @@
"prepare": "node ../../scripts/prepare.js"
},
"dependencies": {
"@storybook/addons": "5.2.0-alpha.34",
"@storybook/api": "5.2.0-alpha.34",
"@storybook/addons": "5.2.0-alpha.39",
"@storybook/api": "5.2.0-alpha.39",
"core-js": "^3.0.1",
"global": "^4.3.2",
"graphiql": "^0.13.0",

View File

@ -1,7 +1,7 @@
import { addons, types } from '@storybook/addons';
import GQL from './manager';
import { ADDON_ID } from '.';
import { ADDON_ID, PARAM_KEY } from '.';
export const register = () => {
addons.register(ADDON_ID, () => {
@ -11,6 +11,7 @@ export const register = () => {
route: ({ storyId }) => `/graphql/${storyId}`,
match: ({ viewMode }) => viewMode === 'graphql',
render: GQL,
paramKey: PARAM_KEY,
});
});
};

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/addon-info",
"version": "5.2.0-alpha.34",
"version": "5.2.0-alpha.39",
"description": "A Storybook addon to show additional information for your stories.",
"keywords": [
"addon",
@ -22,10 +22,10 @@
"prepare": "node ../../scripts/prepare.js"
},
"dependencies": {
"@storybook/addons": "5.2.0-alpha.34",
"@storybook/client-logger": "5.2.0-alpha.34",
"@storybook/components": "5.2.0-alpha.34",
"@storybook/theming": "5.2.0-alpha.34",
"@storybook/addons": "5.2.0-alpha.39",
"@storybook/client-logger": "5.2.0-alpha.39",
"@storybook/components": "5.2.0-alpha.39",
"@storybook/theming": "5.2.0-alpha.39",
"core-js": "^3.0.1",
"global": "^4.3.2",
"jsx-to-string": "^1.4.0",

View File

@ -310,7 +310,7 @@ exports[`addon Info should render <Info /> for memoized component 1`] = `
<div>
It's a
story:
<TestComponent
<Memo(TestComponent)
array={
Array [
1,
@ -368,7 +368,7 @@ exports[`addon Info should render <Info /> for memoized component 1`] = `
</li>
</ul>
</div>
</TestComponent>
</Memo(TestComponent)>
</div>
</div>
<button

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/addon-jest",
"version": "5.2.0-alpha.34",
"version": "5.2.0-alpha.39",
"description": "React storybook addon that show component jest report",
"keywords": [
"addon",
@ -28,11 +28,11 @@
"prepare": "node ../../scripts/prepare.js"
},
"dependencies": {
"@storybook/addons": "5.2.0-alpha.34",
"@storybook/api": "5.2.0-alpha.34",
"@storybook/components": "5.2.0-alpha.34",
"@storybook/core-events": "5.2.0-alpha.34",
"@storybook/theming": "5.2.0-alpha.34",
"@storybook/addons": "5.2.0-alpha.39",
"@storybook/api": "5.2.0-alpha.39",
"@storybook/components": "5.2.0-alpha.39",
"@storybook/core-events": "5.2.0-alpha.39",
"@storybook/theming": "5.2.0-alpha.39",
"core-js": "^3.0.1",
"global": "^4.3.2",
"react": "^16.8.3",

View File

@ -1,9 +1,9 @@
import addons from '@storybook/addons';
import addons, { Parameters } from '@storybook/addons';
import deprecate from 'util-deprecate';
import { normalize, sep } from 'upath';
import { ADD_TESTS } from './shared';
interface AddonParameters {
interface AddonParameters extends Parameters {
jest?: string | string[] | { disable: true };
}

View File

@ -1,6 +1,6 @@
import React from 'react';
import addons from '@storybook/addons';
import { ADDON_ID, PANEL_ID } from './shared';
import { ADDON_ID, PANEL_ID, PARAM_KEY } from './shared';
import Panel from './components/Panel';
@ -8,5 +8,6 @@ addons.register(ADDON_ID, api => {
addons.addPanel(PANEL_ID, {
title: 'tests',
render: ({ active, key }) => <Panel key={key} api={api} active={active} />,
paramKey: PARAM_KEY,
});
});

View File

@ -1,4 +1,5 @@
// addons, panels and events get unique names using a prefix
export const PARAM_KEY = 'test';
export const ADDON_ID = 'storybookjs/test';
export const PANEL_ID = `${ADDON_ID}/panel`;

View File

@ -410,6 +410,9 @@ const groupId = 'GROUP-ID1';
button(label, handler, groupId);
```
Button knobs cause the story to re-render after the handler fires, you can prevent
this by having the handler return false.
### withKnobs options
withKnobs also accepts two optional options as story parameters.

View File

@ -1 +0,0 @@
module.exports = require('./dist/deprecated');

1
addons/knobs/angular.ts Normal file
View File

@ -0,0 +1 @@
export * from './dist/deprecated';

View File

@ -1 +0,0 @@
module.exports = require('./dist/deprecated');

1
addons/knobs/html.ts Normal file
View File

@ -0,0 +1 @@
export * from './dist/deprecated';

View File

@ -1 +0,0 @@
module.exports = require('./dist/deprecated');

1
addons/knobs/marko.ts Normal file
View File

@ -0,0 +1 @@
export * from './dist/deprecated';

View File

@ -1 +0,0 @@
module.exports = require('./dist/deprecated');

1
addons/knobs/mithril.ts Normal file
View File

@ -0,0 +1 @@
export * from './dist/deprecated';

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/addon-knobs",
"version": "5.2.0-alpha.34",
"version": "5.2.0-alpha.39",
"description": "Storybook Addon Prop Editor Component",
"keywords": [
"addon",
@ -17,16 +17,16 @@
},
"license": "MIT",
"main": "dist/index.js",
"jsnext:main": "src/index.js",
"types": "dist/index.d.ts",
"scripts": {
"prepare": "node ../../scripts/prepare.js"
},
"dependencies": {
"@storybook/addons": "5.2.0-alpha.34",
"@storybook/client-api": "5.2.0-alpha.34",
"@storybook/components": "5.2.0-alpha.34",
"@storybook/core-events": "5.2.0-alpha.34",
"@storybook/theming": "5.2.0-alpha.34",
"@storybook/addons": "5.2.0-alpha.39",
"@storybook/client-api": "5.2.0-alpha.39",
"@storybook/components": "5.2.0-alpha.39",
"@storybook/core-events": "5.2.0-alpha.39",
"@storybook/theming": "5.2.0-alpha.39",
"copy-to-clipboard": "^3.0.8",
"core-js": "^3.0.1",
"escape-html": "^1.0.3",
@ -44,5 +44,11 @@
},
"publishConfig": {
"access": "public"
},
"devDependencies": {
"@types/escape-html": "0.0.20",
"@types/react-color": "^3.0.1",
"@types/react-lifecycles-compat": "^3.0.1",
"@types/react-select": "^2.0.19"
}
}

View File

@ -1 +0,0 @@
module.exports = require('./dist/deprecated');

1
addons/knobs/polymer.ts Normal file
View File

@ -0,0 +1 @@
export * from './dist/deprecated';

View File

@ -1 +0,0 @@
module.exports = require('./dist/deprecated');

1
addons/knobs/react.ts Normal file
View File

@ -0,0 +1 @@
export * from './dist/deprecated';

View File

@ -56,6 +56,7 @@ describe('KnobManager', () => {
const newKnob = {
...defaultKnob,
label: 'foo',
defaultValue: defaultKnob.value,
};
@ -86,6 +87,7 @@ describe('KnobManager', () => {
const newKnob = {
...defaultKnob,
label: 'foo',
defaultValue: defaultKnob.value,
};

View File

@ -1,25 +1,33 @@
/* eslint no-underscore-dangle: 0 */
import { navigator } from 'global';
import escape from 'escape-html';
import { getQueryParams } from '@storybook/client-api';
// eslint-disable-next-line import/no-extraneous-dependencies
import { Channel } from '@storybook/channels';
import KnobStore from './KnobStore';
import KnobStore, { Knob } from './KnobStore';
import { SET } from './shared';
import { deserializers } from './converters';
const knobValuesFromUrl = Object.entries(getQueryParams()).reduce((acc, [k, v]) => {
if (k.includes('knob-')) {
return { ...acc, [k.replace('knob-', '')]: v };
}
return acc;
}, {});
const knobValuesFromUrl: Record<string, string> = Object.entries(getQueryParams()).reduce(
(acc, [k, v]) => {
if (k.includes('knob-')) {
return { ...acc, [k.replace('knob-', '')]: v };
}
return acc;
},
{}
);
// This is used by _mayCallChannel to determine how long to wait to before triggering a panel update
const PANEL_UPDATE_INTERVAL = 400;
const escapeStrings = obj => {
function escapeStrings(obj: { [key: string]: string }): { [key: string]: string };
function escapeStrings(obj: (string | string[])[]): (string | string[])[];
function escapeStrings(obj: string): string;
function escapeStrings(obj: any): any {
if (typeof obj === 'string') {
return escape(obj);
}
@ -31,35 +39,45 @@ const escapeStrings = obj => {
const didChange = newArray.some((newValue, key) => newValue !== obj[key]);
return didChange ? newArray : obj;
}
return Object.entries(obj).reduce((acc, [key, oldValue]) => {
return Object.entries<{ [key: string]: string }>(obj).reduce((acc, [key, oldValue]) => {
const newValue = escapeStrings(oldValue);
return newValue === oldValue ? acc : { ...acc, [key]: newValue };
}, obj);
};
}
interface KnobManagerOptions {
escapeHTML?: boolean;
disableDebounce?: boolean;
}
export default class KnobManager {
constructor() {
this.knobStore = new KnobStore();
this.options = {};
}
knobStore = new KnobStore();
setChannel(channel) {
channel: Channel;
options: KnobManagerOptions = {};
calling: boolean;
setChannel(channel: Channel) {
this.channel = channel;
}
setOptions(options) {
setOptions(options: KnobManagerOptions) {
this.options = options;
}
getKnobValue({ value }) {
getKnobValue({ value }: Knob) {
return this.options.escapeHTML ? escapeStrings(value) : value;
}
knob(name, options) {
knob(name: string, options: Knob) {
this._mayCallChannel();
const knobName = options.groupId ? `${name}_${options.groupId}` : name;
const { knobStore } = this;
const existingKnob = knobStore.get(name);
const existingKnob = knobStore.get(knobName);
// We need to return the value set by the knob editor via this.
// Normally the knobs are reset and so re-use is safe as long as the types match
@ -75,24 +93,25 @@ export default class KnobManager {
return this.getKnobValue(existingKnob);
}
const knobInfo = {
const knobInfo: Knob & { name: string; label: string; defaultValue?: any } = {
...options,
name,
name: knobName,
label: name,
};
if (knobValuesFromUrl[name]) {
const value = deserializers[options.type](knobValuesFromUrl[name]);
if (knobValuesFromUrl[knobName]) {
const value = deserializers[options.type](knobValuesFromUrl[knobName]);
knobInfo.defaultValue = value;
knobInfo.value = value;
delete knobValuesFromUrl[name];
delete knobValuesFromUrl[knobName];
} else {
knobInfo.defaultValue = options.value;
}
knobStore.set(name, knobInfo);
return this.getKnobValue(knobStore.get(name));
knobStore.set(knobName, knobInfo);
return this.getKnobValue(knobStore.get(knobName));
}
_mayCallChannel() {

View File

@ -1,56 +0,0 @@
const callArg = fn => fn();
const callAll = fns => fns.forEach(callArg);
export default class KnobStore {
constructor() {
this.store = {};
this.callbacks = [];
}
has(key) {
return this.store[key] !== undefined;
}
set(key, value) {
this.store[key] = value;
this.store[key].used = true;
this.store[key].groupId = value.groupId;
// debounce the execution of the callbacks for 50 milliseconds
if (this.timer) {
clearTimeout(this.timer);
}
this.timer = setTimeout(callAll, 50, this.callbacks);
}
get(key) {
const knob = this.store[key];
if (knob) {
knob.used = true;
}
return knob;
}
getAll() {
return this.store;
}
reset() {
this.store = {};
}
markAllUnused() {
Object.keys(this.store).forEach(knobName => {
this.store[knobName].used = false;
});
}
subscribe(cb) {
this.callbacks.push(cb);
}
unsubscribe(cb) {
const index = this.callbacks.indexOf(cb);
this.callbacks.splice(index, 1);
}
}

View File

@ -0,0 +1,101 @@
import Types, {
TextTypeKnob,
NumberTypeKnob,
ColorTypeKnob,
BooleanTypeKnob,
ObjectTypeKnob,
SelectTypeKnob,
RadiosTypeKnob,
ArrayTypeKnob,
DateTypeKnob,
ButtonTypeOnClickProp,
FileTypeKnob,
OptionsTypeKnob,
} from './components/types';
type Callback = () => any;
type KnobPlus<T extends keyof typeof Types, K> = K & { type: T; groupId?: string };
export type Knob =
| KnobPlus<'text', Pick<TextTypeKnob, 'value'>>
| KnobPlus<'boolean', Pick<BooleanTypeKnob, 'value'>>
| KnobPlus<'number', Pick<NumberTypeKnob, 'value' | 'range' | 'min' | 'max' | 'step'>>
| KnobPlus<'color', Pick<ColorTypeKnob, 'value'>>
| KnobPlus<'object', Pick<ObjectTypeKnob<any>, 'value'>>
| KnobPlus<'select', Pick<SelectTypeKnob, 'value' | 'options'> & { selectV2: true }>
| KnobPlus<'radios', Pick<RadiosTypeKnob, 'value' | 'options'>>
| KnobPlus<'array', Pick<ArrayTypeKnob, 'value' | 'separator'>>
| KnobPlus<'date', Pick<DateTypeKnob, 'value'>>
| KnobPlus<'files', Pick<FileTypeKnob, 'value' | 'accept'>>
| KnobPlus<'button', { value?: unknown; callback: ButtonTypeOnClickProp; hideLabel: true }>
| KnobPlus<'options', Pick<OptionsTypeKnob<any>, 'options' | 'value' | 'optionsObj'>>;
export type KnobStoreKnob = Knob & {
name: string;
label: string;
used?: boolean;
defaultValue?: any;
hideLabel?: boolean;
callback?: () => any;
};
const callArg = (fn: Callback) => fn();
const callAll = (fns: Callback[]) => fns.forEach(callArg);
export default class KnobStore {
store: Record<string, KnobStoreKnob> = {};
callbacks: Callback[] = [];
timer: NodeJS.Timeout;
has(key: string) {
return this.store[key] !== undefined;
}
set(key: string, value: KnobStoreKnob) {
this.store[key] = {
...value,
used: true,
groupId: value.groupId,
};
// debounce the execution of the callbacks for 50 milliseconds
if (this.timer) {
clearTimeout(this.timer);
}
this.timer = setTimeout(callAll, 50, this.callbacks);
}
get(key: string) {
const knob = this.store[key];
if (knob) {
knob.used = true;
}
return knob;
}
getAll() {
return this.store;
}
reset() {
this.store = {};
}
markAllUnused() {
Object.keys(this.store).forEach(knobName => {
this.store[knobName].used = false;
});
}
subscribe(cb: Callback) {
this.callbacks.push(cb);
}
unsubscribe(cb: Callback) {
const index = this.callbacks.indexOf(cb);
this.callbacks.splice(index, 1);
}
}

View File

@ -1,4 +1,4 @@
import React, { PureComponent, Fragment } from 'react';
import React, { PureComponent, Fragment, ComponentType } from 'react';
import PropTypes from 'prop-types';
import qs from 'qs';
import { document } from 'global';
@ -18,6 +18,7 @@ import { RESET, SET, CHANGE, SET_OPTIONS, CLICK } from '../shared';
import Types from './types';
import PropForm from './PropForm';
import { KnobStoreKnob } from '../KnobStore';
const getTimestamp = () => +new Date();
@ -32,17 +33,60 @@ const PanelWrapper = styled(({ children, className }) => (
width: '100%',
});
export default class KnobPanel extends PureComponent {
constructor(props) {
super(props);
this.state = {
knobs: {},
};
this.options = {};
interface PanelKnobGroups {
title: string;
render: (knob: any) => any;
}
this.lastEdit = getTimestamp();
this.loadedFromUrl = false;
}
interface KnobPanelProps {
active: boolean;
onReset?: object;
api: {
on: Function;
off: Function;
emit: Function;
getQueryParam: Function;
setQueryParams: Function;
};
}
interface KnobPanelState {
knobs: Record<string, KnobStoreKnob>;
}
interface KnobPanelOptions {
timestamps?: boolean;
}
type KnobControlType = ComponentType<any> & {
serialize: (v: any) => any;
deserialize: (v: any) => any;
};
export default class KnobPanel extends PureComponent<KnobPanelProps> {
static propTypes = {
active: PropTypes.bool.isRequired,
onReset: PropTypes.object, // eslint-disable-line
api: PropTypes.shape({
on: PropTypes.func,
getQueryParam: PropTypes.func,
setQueryParams: PropTypes.func,
}).isRequired,
};
state: KnobPanelState = {
knobs: {},
};
options: KnobPanelOptions = {};
lastEdit: number = getTimestamp();
loadedFromUrl = false;
mounted = false;
stopListeningOnStory: Function;
componentDidMount() {
this.mounted = true;
@ -66,12 +110,18 @@ export default class KnobPanel extends PureComponent {
this.stopListeningOnStory();
}
setOptions = (options = { timestamps: false }) => {
setOptions = (options: KnobPanelOptions = { timestamps: false }) => {
this.options = options;
};
setKnobs = ({ knobs, timestamp }) => {
const queryParams = {};
setKnobs = ({
knobs,
timestamp,
}: {
knobs: Record<string, KnobStoreKnob>;
timestamp?: number;
}) => {
const queryParams: Record<string, any> = {};
const { api } = this.props;
if (!this.options.timestamps || !timestamp || this.lastEdit <= timestamp) {
@ -83,9 +133,9 @@ export default class KnobPanel extends PureComponent {
// If the knob value present in url
if (urlValue !== undefined) {
const value = Types[knob.type].deserialize(urlValue);
const value = (Types[knob.type] as KnobControlType).deserialize(urlValue);
knob.value = value;
queryParams[`knob-${name}`] = Types[knob.type].serialize(value);
queryParams[`knob-${name}`] = (Types[knob.type] as KnobControlType).serialize(value);
api.emit(CHANGE, knob);
}
@ -111,7 +161,7 @@ export default class KnobPanel extends PureComponent {
const { knobs } = this.state;
Object.entries(knobs).forEach(([name, knob]) => {
query[`knob-${name}`] = Types[knob.type].serialize(knob.value);
query[`knob-${name}`] = (Types[knob.type] as KnobControlType).serialize(knob.value);
});
copy(`${location.origin + location.pathname}?${qs.stringify(query, { encode: false })}`);
@ -119,13 +169,13 @@ export default class KnobPanel extends PureComponent {
// TODO: show some notification of this
};
emitChange = changedKnob => {
emitChange = (changedKnob: KnobStoreKnob) => {
const { api } = this.props;
api.emit(CHANGE, changedKnob);
};
handleChange = changedKnob => {
handleChange = (changedKnob: KnobStoreKnob) => {
this.lastEdit = getTimestamp();
const { api } = this.props;
const { knobs } = this.state;
@ -139,18 +189,18 @@ export default class KnobPanel extends PureComponent {
this.setState({ knobs: newKnobs }, () => {
this.emitChange(changedKnob);
const queryParams = {};
const queryParams: { [key: string]: any } = {};
Object.keys(newKnobs).forEach(n => {
const knob = newKnobs[n];
queryParams[`knob-${n}`] = Types[knob.type].serialize(knob.value);
queryParams[`knob-${n}`] = (Types[knob.type] as KnobControlType).serialize(knob.value);
});
api.setQueryParams(queryParams);
});
};
handleClick = knob => {
handleClick = (knob: KnobStoreKnob) => {
const { api } = this.props;
api.emit(CLICK, knob);
@ -163,8 +213,8 @@ export default class KnobPanel extends PureComponent {
return null;
}
const groups = {};
const groupIds = [];
const groups: Record<string, PanelKnobGroups> = {};
const groupIds: string[] = [];
const knobKeysArray = Object.keys(knobs).filter(key => knobs[key].used);
@ -175,8 +225,6 @@ export default class KnobPanel extends PureComponent {
render: ({ active }) => (
<TabWrapper key={knobKeyGroupId} active={active}>
<PropForm
// false positive
// eslint-disable-next-line no-use-before-define
knobs={knobsArray.filter(
knob => (knob.groupId || DEFAULT_GROUP_ID) === knobKeyGroupId
)}
@ -210,12 +258,12 @@ export default class KnobPanel extends PureComponent {
}
// Always sort DEFAULT_GROUP_ID (ungrouped) tab last without changing the remaining tabs
const sortEntries = g => {
const sortEntries = (g: Record<string, PanelKnobGroups>): [string, PanelKnobGroups][] => {
const unsortedKeys = Object.keys(g);
if (unsortedKeys.indexOf(DEFAULT_GROUP_ID) !== -1) {
const sortedKeys = unsortedKeys.filter(key => key !== DEFAULT_GROUP_ID);
sortedKeys.push(DEFAULT_GROUP_ID);
return sortedKeys.map(key => [key, g[key]]);
return sortedKeys.map<[string, PanelKnobGroups]>(key => [key, g[key]]);
}
return Object.entries(g);
};
@ -251,13 +299,3 @@ export default class KnobPanel extends PureComponent {
);
}
}
KnobPanel.propTypes = {
active: PropTypes.bool.isRequired,
onReset: PropTypes.object, // eslint-disable-line
api: PropTypes.shape({
on: PropTypes.func,
getQueryParam: PropTypes.func,
setQueryParams: PropTypes.func,
}).isRequired,
};

View File

@ -1,50 +0,0 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Form } from '@storybook/components';
import TypeMap from './types';
const InvalidType = () => <span>Invalid Type</span>;
export default class PropForm extends Component {
makeChangeHandler(name, type) {
const { onFieldChange } = this.props;
return (value = '') => {
const change = { name, type, value };
onFieldChange(change);
};
}
render() {
const { knobs, onFieldClick } = this.props;
return (
<Form>
{knobs.map(knob => {
const changeHandler = this.makeChangeHandler(knob.name, knob.type);
const InputType = TypeMap[knob.type] || InvalidType;
return (
<Form.Field key={knob.name} label={!knob.hideLabel && `${knob.name}`}>
<InputType knob={knob} onChange={changeHandler} onClick={onFieldClick} />
</Form.Field>
);
})}
</Form>
);
}
}
PropForm.displayName = 'PropForm';
PropForm.propTypes = {
knobs: PropTypes.arrayOf(
PropTypes.shape({
name: PropTypes.string,
value: PropTypes.any,
})
).isRequired,
onFieldChange: PropTypes.func.isRequired,
onFieldClick: PropTypes.func.isRequired,
};

View File

@ -0,0 +1,62 @@
import React, { Component, WeakValidationMap, ComponentType, Requireable } from 'react';
import PropTypes from 'prop-types';
import { Form } from '@storybook/components';
import TypeMap from './types';
import { KnobStoreKnob } from '../KnobStore';
interface PropFormProps {
knobs: KnobStoreKnob[];
onFieldChange: Function;
onFieldClick: Function;
}
const InvalidType = () => <span>Invalid Type</span>;
export default class PropForm extends Component<PropFormProps> {
static displayName = 'PropForm';
static defaultProps = {
knobs: [] as KnobStoreKnob[],
};
static propTypes: WeakValidationMap<PropFormProps> = {
// TODO: remove `any` once DefinitelyTyped/DefinitelyTyped#31280 has been resolved
knobs: PropTypes.arrayOf(
PropTypes.shape({
name: PropTypes.string,
value: PropTypes.any,
})
).isRequired as Requireable<any[]>,
onFieldChange: PropTypes.func.isRequired,
onFieldClick: PropTypes.func.isRequired,
};
makeChangeHandler(name: string, type: string) {
const { onFieldChange } = this.props;
return (value = '') => {
const change = { name, type, value };
onFieldChange(change);
};
}
render() {
const { knobs, onFieldClick } = this.props;
return (
<Form>
{knobs.map(knob => {
const changeHandler = this.makeChangeHandler(knob.name, knob.type);
const InputType: ComponentType<any> = TypeMap[knob.type] || InvalidType;
return (
<Form.Field key={knob.name} label={!knob.hideLabel && `${knob.label || knob.name}`}>
<InputType knob={knob} onChange={changeHandler} onClick={onFieldClick} />
</Form.Field>
);
})}
</Form>
);
}
}

View File

@ -1,67 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import { Form } from '@storybook/components';
function formatArray(value, separator) {
if (value === '') {
return [];
}
return value.split(separator);
}
class ArrayType extends React.Component {
shouldComponentUpdate(nextProps) {
const { knob } = this.props;
return nextProps.knob.value !== knob.value;
}
handleChange = e => {
const { knob, onChange } = this.props;
const { value } = e.target;
const newVal = formatArray(value, knob.separator);
onChange(newVal);
};
render() {
const { knob } = this.props;
const value = knob.value.join(knob.separator);
return (
<Form.Textarea
id={knob.name}
name={knob.name}
value={value}
onChange={this.handleChange}
size="flex"
/>
);
}
}
ArrayType.defaultProps = {
knob: {},
onChange: value => value,
};
ArrayType.propTypes = {
knob: PropTypes.shape({
name: PropTypes.string,
value: PropTypes.array,
separator: PropTypes.string,
}),
onChange: PropTypes.func,
};
ArrayType.serialize = value => value;
ArrayType.deserialize = value => {
if (Array.isArray(value)) return value;
return Object.keys(value)
.sort()
.reduce((array, key) => [...array, value[key]], []);
};
export default ArrayType;

View File

@ -0,0 +1,80 @@
import PropTypes from 'prop-types';
import React, { Component, WeakValidationMap } from 'react';
import { Form } from '@storybook/components';
type ArrayTypeKnobValue = string[];
export interface ArrayTypeKnob {
name: string;
value: ArrayTypeKnobValue;
separator: string;
}
interface ArrayTypeProps {
knob: ArrayTypeKnob;
onChange: (value: ArrayTypeKnobValue) => ArrayTypeKnobValue;
}
function formatArray(value: string, separator: string) {
if (value === '') {
return [];
}
return value.split(separator);
}
export default class ArrayType extends Component<ArrayTypeProps> {
static defaultProps: Partial<ArrayTypeProps> = {
knob: {} as any,
onChange: (value: ArrayTypeKnobValue) => value,
};
static propTypes: WeakValidationMap<ArrayTypeProps> = {
// TODO: remove `any` once DefinitelyTyped/DefinitelyTyped#31280 has been resolved
knob: PropTypes.shape({
name: PropTypes.string,
value: PropTypes.array,
separator: PropTypes.string,
}) as any,
onChange: PropTypes.func,
};
static serialize = (value: ArrayTypeKnobValue) => value;
static deserialize = (value: ArrayTypeKnobValue) => {
if (Array.isArray(value)) return value;
return Object.keys(value)
.sort()
.reduce((array, key) => [...array, value[key]], []);
};
shouldComponentUpdate(nextProps: Readonly<ArrayTypeProps>) {
const { knob } = this.props;
return nextProps.knob.value !== knob.value;
}
handleChange = (e: Event) => {
const { knob, onChange } = this.props;
const { value } = e.target as HTMLTextAreaElement;
const newVal = formatArray(value, knob.separator);
onChange(newVal);
};
render() {
const { knob } = this.props;
const value = knob.value.join(knob.separator);
return (
<Form.Textarea
id={knob.name}
name={knob.name}
value={value}
onChange={this.handleChange}
size="flex"
/>
);
}
}

View File

@ -1,43 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import { styled } from '@storybook/theming';
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 BooleanType = ({ knob, onChange }) => (
<Input
id={knob.name}
name={knob.name}
type="checkbox"
onChange={e => onChange(e.target.checked)}
checked={knob.value}
/>
);
BooleanType.defaultProps = {
knob: {},
onChange: value => value,
};
BooleanType.propTypes = {
knob: PropTypes.shape({
name: PropTypes.string,
value: PropTypes.bool,
}),
onChange: PropTypes.func,
};
BooleanType.serialize = value => (value ? String(value) : null);
BooleanType.deserialize = value => value === 'true';
export default BooleanType;

View File

@ -0,0 +1,63 @@
import PropTypes from 'prop-types';
import React, { FunctionComponent } from 'react';
import { styled } from '@storybook/theming';
type BooleanTypeKnobValue = boolean;
export interface BooleanTypeKnob {
name: string;
value: BooleanTypeKnobValue;
separator: string;
}
export interface BooleanTypeProps {
knob: BooleanTypeKnob;
onChange: (value: BooleanTypeKnobValue) => BooleanTypeKnobValue;
}
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 serialize = (value: BooleanTypeKnobValue): string | null => (value ? String(value) : null);
const deserialize = (value: string | null) => value === 'true';
const BooleanType: FunctionComponent<BooleanTypeProps> & {
serialize: typeof serialize;
deserialize: typeof deserialize;
} = ({ knob, onChange }) => (
<Input
id={knob.name}
name={knob.name}
type="checkbox"
onChange={e => onChange(e.target.checked)}
checked={knob.value}
/>
);
BooleanType.defaultProps = {
knob: {} as any,
onChange: value => value,
};
BooleanType.propTypes = {
// TODO: remove `any` once DefinitelyTyped/DefinitelyTyped#31280 has been resolved
knob: PropTypes.shape({
name: PropTypes.string,
value: PropTypes.bool,
}) as any,
onChange: PropTypes.func,
};
BooleanType.serialize = serialize;
BooleanType.deserialize = deserialize;
export default BooleanType;

View File

@ -1,22 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import { Form } from '@storybook/components';
const ButtonType = ({ knob, onClick }) => (
<Form.Button type="button" name={knob.name} onClick={() => onClick(knob)}>
{knob.name}
</Form.Button>
);
ButtonType.propTypes = {
knob: PropTypes.shape({
name: PropTypes.string,
}).isRequired,
onClick: PropTypes.func.isRequired,
};
ButtonType.serialize = () => undefined;
ButtonType.deserialize = () => undefined;
export default ButtonType;

View File

@ -0,0 +1,45 @@
import PropTypes from 'prop-types';
import React, { FunctionComponent, Validator } from 'react';
import { Form } from '@storybook/components';
export interface ButtonTypeKnob {
name: string;
value: unknown;
}
export type ButtonTypeOnClickProp = (knob: ButtonTypeKnob) => any;
export interface ButtonTypeProps {
knob: ButtonTypeKnob;
onClick: ButtonTypeOnClickProp;
}
const serialize = (): undefined => undefined;
const deserialize = (): undefined => undefined;
const ButtonType: FunctionComponent<ButtonTypeProps> & {
serialize: typeof serialize;
deserialize: typeof deserialize;
} = ({ knob, onClick }) => (
<Form.Button type="button" name={knob.name} onClick={() => onClick(knob)}>
{knob.name}
</Form.Button>
);
ButtonType.defaultProps = {
knob: {} as any,
};
ButtonType.propTypes = {
// TODO: remove `any` once DefinitelyTyped/DefinitelyTyped#31280 has been resolved
knob: PropTypes.shape({
name: PropTypes.string,
}).isRequired as Validator<any>,
onClick: PropTypes.func.isRequired,
};
ButtonType.serialize = serialize;
ButtonType.deserialize = deserialize;
export default ButtonType;

View File

@ -1,110 +0,0 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { styled } from '@storybook/theming';
const CheckboxesWrapper = styled.div(({ 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',
});
class CheckboxesType extends Component {
constructor(props) {
super(props);
const { knob } = props;
this.state = {
values: knob.defaultValue || [],
};
}
handleChange = e => {
const { onChange } = this.props;
const currentValue = e.target.value;
const { values } = this.state;
if (values.includes(currentValue)) {
values.splice(values.indexOf(currentValue), 1);
} else {
values.push(currentValue);
}
this.setState({ values });
onChange(values);
};
renderCheckboxList = ({ options }) =>
Object.keys(options).map(key => this.renderCheckbox(key, options[key]));
renderCheckbox = (label, value) => {
const { knob } = this.props;
const { name } = knob;
const id = `${name}-${value}`;
const { values } = this.state;
return (
<div key={id}>
<input
type="checkbox"
id={id}
name={name}
value={value}
onChange={this.handleChange}
checked={values.includes(value)}
/>
<CheckboxLabel htmlFor={id}>{label}</CheckboxLabel>
</div>
);
};
render() {
const { knob, isInline } = this.props;
return (
<CheckboxFieldset>
<CheckboxesWrapper isInline={isInline}>{this.renderCheckboxList(knob)}</CheckboxesWrapper>
</CheckboxFieldset>
);
}
}
CheckboxesType.defaultProps = {
knob: {},
onChange: value => value,
isInline: false,
};
CheckboxesType.propTypes = {
knob: PropTypes.shape({
name: PropTypes.string,
value: PropTypes.array,
options: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
}),
onChange: PropTypes.func,
isInline: PropTypes.bool,
};
CheckboxesType.serialize = value => value;
CheckboxesType.deserialize = value => value;
export default CheckboxesType;

View File

@ -0,0 +1,135 @@
import React, { Component, ChangeEvent, WeakValidationMap } from 'react';
import PropTypes from 'prop-types';
import { styled } from '@storybook/theming';
type CheckboxesTypeKnobValue = string[];
interface CheckboxesWrapperProps {
isInline: boolean;
}
export interface CheckboxesTypeKnob {
name: string;
value: CheckboxesTypeKnobValue;
defaultValue: CheckboxesTypeKnobValue;
options: {
[key: string]: string;
};
}
interface CheckboxesTypeProps {
knob: CheckboxesTypeKnob;
isInline: boolean;
onChange: (value: CheckboxesTypeKnobValue) => CheckboxesTypeKnobValue;
}
interface CheckboxesTypeState {
values: CheckboxesTypeKnobValue;
}
const CheckboxesWrapper = styled.div(({ isInline }: CheckboxesWrapperProps) =>
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',
});
export default class CheckboxesType extends Component<CheckboxesTypeProps, CheckboxesTypeState> {
static defaultProps: CheckboxesTypeProps = {
knob: {} as any,
onChange: value => value,
isInline: false,
};
static propTypes: WeakValidationMap<CheckboxesTypeProps> = {
// TODO: remove `any` once DefinitelyTyped/DefinitelyTyped#31280 has been resolved
knob: PropTypes.shape({
name: PropTypes.string,
value: PropTypes.array,
options: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
}) as any,
onChange: PropTypes.func,
isInline: PropTypes.bool,
};
static serialize = (value: CheckboxesTypeKnobValue) => value;
static deserialize = (value: CheckboxesTypeKnobValue) => value;
constructor(props: CheckboxesTypeProps) {
super(props);
const { knob } = props;
this.state = {
values: knob.defaultValue || [],
};
}
handleChange = (e: ChangeEvent<HTMLInputElement>) => {
const { onChange } = this.props;
const currentValue = (e.target as HTMLInputElement).value;
const { values } = this.state;
if (values.includes(currentValue)) {
values.splice(values.indexOf(currentValue), 1);
} else {
values.push(currentValue);
}
this.setState({ values });
onChange(values);
};
renderCheckboxList = ({ options }: CheckboxesTypeKnob) =>
Object.keys(options).map(key => this.renderCheckbox(key, options[key]));
renderCheckbox = (label: string, value: string) => {
const { knob } = this.props;
const { name } = knob;
const id = `${name}-${value}`;
const { values } = this.state;
return (
<div key={id}>
<input
type="checkbox"
id={id}
name={name}
value={value}
onChange={this.handleChange}
checked={values.includes(value)}
/>
<CheckboxLabel htmlFor={id}>{label}</CheckboxLabel>
</div>
);
};
render() {
const { knob, isInline } = this.props;
return (
<CheckboxFieldset>
<CheckboxesWrapper isInline={isInline}>{this.renderCheckboxList(knob)}</CheckboxesWrapper>
</CheckboxFieldset>
);
}
}

View File

@ -1,15 +1,38 @@
import { document } from 'global';
import PropTypes from 'prop-types';
import React from 'react';
import { SketchPicker } from 'react-color';
import React, { Component, WeakValidationMap } from 'react';
import { SketchPicker, ColorResult } from 'react-color';
import { styled } from '@storybook/theming';
import { Form } from '@storybook/components';
type ColorTypeKnobValue = string;
export interface ColorTypeKnob {
name: string;
value: ColorTypeKnobValue;
}
interface ColorTypeProps {
knob: ColorTypeKnob;
onChange: (value: ColorTypeKnobValue) => ColorTypeKnobValue;
}
interface ColorTypeState {
displayColorPicker: boolean;
}
interface ColorButtonProps {
name: string;
type: string;
size: string;
active: boolean;
onClick: () => any;
}
const { Button } = Form;
const Swatch = styled.div(({ theme }) => ({
const Swatch = styled.div<{}>(({ theme }) => ({
position: 'absolute',
top: '50%',
transform: 'translateY(-50%)',
@ -20,25 +43,45 @@ const Swatch = styled.div(({ theme }) => ({
borderRadius: '1rem',
}));
const ColorButton = styled(Button)(({ active }) => ({
const ColorButton = styled(Button)(({ active }: ColorButtonProps) => ({
zIndex: active ? 3 : 'unset',
}));
const Popover = styled.div({
position: 'absolute',
zIndex: '2',
zIndex: 2,
});
class ColorType extends React.Component {
state = {
export default class ColorType extends Component<ColorTypeProps, ColorTypeState> {
static propTypes: WeakValidationMap<ColorTypeProps> = {
// TODO: remove `any` once DefinitelyTyped/DefinitelyTyped#31280 has been resolved
knob: PropTypes.shape({
name: PropTypes.string,
value: PropTypes.string,
}) as any,
onChange: PropTypes.func,
};
static defaultProps: ColorTypeProps = {
knob: {} as any,
onChange: value => value,
};
static serialize = (value: ColorTypeKnobValue) => value;
static deserialize = (value: ColorTypeKnobValue) => value;
state: ColorTypeState = {
displayColorPicker: false,
};
popover: HTMLDivElement;
componentDidMount() {
document.addEventListener('mousedown', this.handleWindowMouseDown);
}
shouldComponentUpdate(nextProps, nextState) {
shouldComponentUpdate(nextProps: ColorTypeProps, nextState: ColorTypeState) {
const { knob } = this.props;
const { displayColorPicker } = this.state;
@ -51,9 +94,9 @@ class ColorType extends React.Component {
document.removeEventListener('mousedown', this.handleWindowMouseDown);
}
handleWindowMouseDown = e => {
handleWindowMouseDown = (e: MouseEvent) => {
const { displayColorPicker } = this.state;
if (!displayColorPicker || this.popover.contains(e.target)) {
if (!displayColorPicker || this.popover.contains(e.target as HTMLElement)) {
return;
}
@ -70,7 +113,7 @@ class ColorType extends React.Component {
});
};
handleChange = color => {
handleChange = (color: ColorResult) => {
const { onChange } = this.props;
onChange(`rgba(${color.rgb.r},${color.rgb.g},${color.rgb.b},${color.rgb.a})`);
@ -105,20 +148,3 @@ class ColorType extends React.Component {
);
}
}
ColorType.propTypes = {
knob: PropTypes.shape({
name: PropTypes.string,
value: PropTypes.string,
}),
onChange: PropTypes.func,
};
ColorType.defaultProps = {
knob: {},
onChange: value => value,
};
ColorType.serialize = value => value;
ColorType.deserialize = value => value;
export default ColorType;

View File

@ -1,8 +1,24 @@
import React, { Component } from 'react';
import React, { Component, ChangeEvent, WeakValidationMap } from 'react';
import PropTypes from 'prop-types';
import { styled } from '@storybook/theming';
import { Form } from '@storybook/components';
type DateTypeKnobValue = number;
export interface DateTypeKnob {
name: string;
value: DateTypeKnobValue;
}
interface DateTypeProps {
knob: DateTypeKnob;
onChange: (value: DateTypeKnobValue) => DateTypeKnobValue;
}
interface DateTypeState {
valid: boolean | undefined;
}
const FlexSpaced = styled.div({
flex: 1,
display: 'flex',
@ -15,29 +31,54 @@ const FlexSpaced = styled.div({
});
const FlexInput = styled(Form.Input)({ flex: 1 });
const formatDate = date => {
const formatDate = (date: Date) => {
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 = date => {
const formatTime = (date: Date) => {
const hours = `0${date.getHours()}`.slice(-2);
const minutes = `0${date.getMinutes()}`.slice(-2);
return `${hours}:${minutes}`;
};
class DateType extends Component {
export default class DateType extends Component<DateTypeProps, DateTypeState> {
static defaultProps: DateTypeProps = {
knob: {} as any,
onChange: value => value,
};
static propTypes: WeakValidationMap<DateTypeProps> = {
// TODO: remove `any` once DefinitelyTyped/DefinitelyTyped#31280 has been resolved
knob: PropTypes.shape({
name: PropTypes.string,
value: PropTypes.number,
}) as any,
onChange: PropTypes.func,
};
static serialize = (value: DateTypeKnobValue) =>
new Date(value).getTime() || new Date().getTime();
static deserialize = (value: DateTypeKnobValue) =>
new Date(value).getTime() || new Date().getTime();
static getDerivedStateFromProps() {
return { valid: true };
}
state = {
state: DateTypeState = {
valid: undefined,
};
dateInput: HTMLInputElement;
timeInput: HTMLInputElement;
componentDidUpdate() {
const { knob } = this.props;
const { valid } = this.state;
@ -49,7 +90,7 @@ class DateType extends Component {
}
}
onDateChange = e => {
onDateChange = (e: ChangeEvent<HTMLInputElement>) => {
const { knob, onChange } = this.props;
const { state } = this;
@ -70,7 +111,7 @@ class DateType extends Component {
}
};
onTimeChange = e => {
onTimeChange = (e: ChangeEvent<HTMLInputElement>) => {
const { knob, onChange } = this.props;
const { state } = this;
@ -100,7 +141,7 @@ class DateType extends Component {
<FlexInput
type="date"
max="9999-12-31" // I do this because of a rendering bug in chrome
ref={el => {
ref={(el: HTMLInputElement) => {
this.dateInput = el;
}}
id={`${name}date`}
@ -111,7 +152,7 @@ class DateType extends Component {
type="time"
id={`${name}time`}
name={`${name}time`}
ref={el => {
ref={(el: HTMLInputElement) => {
this.timeInput = el;
}}
onChange={this.onTimeChange}
@ -121,21 +162,3 @@ class DateType extends Component {
) : null;
}
}
DateType.defaultProps = {
knob: {},
onChange: value => value,
};
DateType.propTypes = {
knob: PropTypes.shape({
name: PropTypes.string,
value: PropTypes.number,
}),
onChange: PropTypes.func,
};
DateType.serialize = value => new Date(value).getTime() || new Date().getTime();
DateType.deserialize = value => new Date(value).getTime() || new Date().getTime();
export default DateType;

View File

@ -1,46 +0,0 @@
import { FileReader } from 'global';
import PropTypes from 'prop-types';
import React from 'react';
import { styled } from '@storybook/theming';
import { Form } from '@storybook/components';
const FileInput = styled(Form.Input)({
paddingTop: 4,
});
function fileReaderPromise(file) {
return new Promise(resolve => {
const fileReader = new FileReader();
fileReader.onload = e => resolve(e.currentTarget.result);
fileReader.readAsDataURL(file);
});
}
const FilesType = ({ knob, onChange }) => (
<FileInput
type="file"
name={knob.name}
multiple
onChange={e => Promise.all(Array.from(e.target.files).map(fileReaderPromise)).then(onChange)}
accept={knob.accept}
size="flex"
/>
);
FilesType.defaultProps = {
knob: {},
onChange: value => value,
};
FilesType.propTypes = {
knob: PropTypes.shape({
name: PropTypes.string,
}),
onChange: PropTypes.func,
};
FilesType.serialize = () => undefined;
FilesType.deserialize = () => undefined;
export default FilesType;

View File

@ -0,0 +1,68 @@
import { FileReader } from 'global';
import PropTypes from 'prop-types';
import React, { ChangeEvent, FunctionComponent } from 'react';
import { styled } from '@storybook/theming';
import { Form } from '@storybook/components';
type DateTypeKnobValue = string[];
export interface FileTypeKnob {
name: string;
accept: string;
value: DateTypeKnobValue;
}
export interface FilesTypeProps {
knob: FileTypeKnob;
onChange: (value: DateTypeKnobValue) => DateTypeKnobValue;
}
const FileInput = styled(Form.Input)({
paddingTop: 4,
});
function fileReaderPromise(file: File) {
return new Promise<string>(resolve => {
const fileReader = new FileReader();
fileReader.onload = (e: Event) => resolve((e.currentTarget as FileReader).result as string);
fileReader.readAsDataURL(file);
});
}
const serialize = (): undefined => undefined;
const deserialize = (): undefined => undefined;
const FilesType: FunctionComponent<FilesTypeProps> & {
serialize: typeof serialize;
deserialize: typeof deserialize;
} = ({ knob, onChange }) => (
<FileInput
type="file"
name={knob.name}
multiple
onChange={(e: ChangeEvent<HTMLInputElement>) =>
Promise.all(Array.from(e.target.files).map(fileReaderPromise)).then(onChange)
}
accept={knob.accept}
size="flex"
/>
);
FilesType.defaultProps = {
knob: {} as any,
onChange: value => value,
};
FilesType.propTypes = {
// TODO: remove `any` once DefinitelyTyped/DefinitelyTyped#31280 has been resolved
knob: PropTypes.shape({
name: PropTypes.string,
}) as any,
onChange: PropTypes.func,
};
FilesType.serialize = serialize;
FilesType.deserialize = deserialize;
export default FilesType;

View File

@ -1,102 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import { styled } from '@storybook/theming';
import { Form } from '@storybook/components';
const base = {
boxSizing: 'border-box',
height: '25px',
outline: 'none',
border: '1px solid #f7f4f4',
borderRadius: 2,
fontSize: 11,
padding: '5px',
color: '#444',
};
const RangeInput = styled.input(base, {
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%',
});
class NumberType extends React.Component {
shouldComponentUpdate(nextProps) {
const { knob } = this.props;
return nextProps.knob.value !== knob.value;
}
handleChange = event => {
const { onChange } = this.props;
const { value } = event.target;
let parsedValue = Number(value);
if (Number.isNaN(parsedValue) || value === '') {
parsedValue = null;
}
onChange(parsedValue);
};
render() {
const { knob } = this.props;
return knob.range ? (
<RangeWrapper>
<RangeLabel>{knob.min}</RangeLabel>
<RangeInput
value={knob.value}
type="range"
name={knob.name}
min={knob.min}
max={knob.max}
step={knob.step}
onChange={this.handleChange}
/>
<RangeLabel>{`${knob.value} / ${knob.max}`}</RangeLabel>
</RangeWrapper>
) : (
<Form.Input
value={knob.value}
type="number"
name={knob.name}
min={knob.min}
max={knob.max}
step={knob.step}
onChange={this.handleChange}
size="flex"
/>
);
}
}
NumberType.propTypes = {
knob: PropTypes.shape({
name: PropTypes.string,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
range: PropTypes.bool,
min: PropTypes.number,
max: PropTypes.number,
step: PropTypes.number,
}).isRequired,
onChange: PropTypes.func.isRequired,
};
NumberType.serialize = value => (value === null || value === undefined ? '' : String(value));
NumberType.deserialize = value => (value === '' ? null : parseFloat(value));
export default NumberType;

View File

@ -0,0 +1,123 @@
import PropTypes from 'prop-types';
import React, { Component, ChangeEvent } from 'react';
import { styled } from '@storybook/theming';
import { Form } from '@storybook/components';
type NumberTypeKnobValue = number;
export interface NumberTypeKnobOptions {
range?: boolean;
min?: number;
max?: number;
step?: number;
}
export interface NumberTypeKnob extends NumberTypeKnobOptions {
name: string;
value: number;
}
interface NumberTypeProps {
knob: NumberTypeKnob;
onChange: (value: NumberTypeKnobValue) => NumberTypeKnobValue;
}
const RangeInput = styled.input(
{
boxSizing: 'border-box',
height: '25px',
outline: 'none',
border: '1px solid #f7f4f4',
borderRadius: 2,
fontSize: 11,
padding: '5px',
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 default class NumberType extends Component<NumberTypeProps> {
static propTypes = {
knob: PropTypes.shape({
name: PropTypes.string,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
range: PropTypes.bool,
min: PropTypes.number,
max: PropTypes.number,
step: PropTypes.number,
}).isRequired,
onChange: PropTypes.func.isRequired,
};
static serialize = (value: NumberTypeKnobValue | null | undefined) =>
value === null || value === undefined ? '' : String(value);
static deserialize = (value: string) => (value === '' ? null : parseFloat(value));
shouldComponentUpdate(nextProps: NumberTypeProps) {
const { knob } = this.props;
return nextProps.knob.value !== knob.value;
}
handleChange = (event: ChangeEvent<HTMLInputElement>) => {
const { onChange } = this.props;
const { value } = event.target;
let parsedValue = Number(value);
if (Number.isNaN(parsedValue) || value === '') {
parsedValue = null;
}
onChange(parsedValue);
};
render() {
const { knob } = this.props;
return knob.range ? (
<RangeWrapper>
<RangeLabel>{knob.min}</RangeLabel>
<RangeInput
value={knob.value}
type="range"
name={knob.name}
min={knob.min}
max={knob.max}
step={knob.step}
onChange={this.handleChange}
/>
<RangeLabel>{`${knob.value} / ${knob.max}`}</RangeLabel>
</RangeWrapper>
) : (
<Form.Input
value={knob.value}
type="number"
name={knob.name}
min={knob.min}
max={knob.max}
step={knob.step}
onChange={this.handleChange}
size="flex"
/>
);
}
}

View File

@ -1,17 +1,42 @@
import React, { Component } from 'react';
import React, { Component, ChangeEvent } from 'react';
import PropTypes from 'prop-types';
import deepEqual from 'fast-deep-equal';
import { polyfill } from 'react-lifecycles-compat';
import { Form } from '@storybook/components';
class ObjectType extends Component {
state = {
value: {},
failed: false,
json: '',
export interface ObjectTypeKnob<T> {
name: string;
value: T;
}
interface ObjectTypeProps<T> {
knob: ObjectTypeKnob<T>;
onChange: (value: T) => T;
}
interface ObjectTypeState<T> {
value: string;
failed: boolean;
json?: T;
}
class ObjectType<T> extends Component<ObjectTypeProps<T>> {
static propTypes = {
knob: PropTypes.shape({
name: PropTypes.string,
value: PropTypes.oneOfType([PropTypes.object, PropTypes.array]),
}).isRequired,
onChange: PropTypes.func.isRequired,
};
static getDerivedStateFromProps(props, state) {
static serialize: { <T>(object: T): string } = object => JSON.stringify(object);
static deserialize: { <T>(value: string): T } = value => (value ? JSON.parse(value) : {});
static getDerivedStateFromProps<T>(
props: ObjectTypeProps<T>,
state: ObjectTypeState<T>
): ObjectTypeState<T> {
if (!deepEqual(props.knob.value, state.json)) {
try {
return {
@ -26,7 +51,13 @@ class ObjectType extends Component {
return null;
}
handleChange = e => {
state: ObjectTypeState<T> = {
value: '',
failed: false,
json: {} as any,
};
handleChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
const { value } = e.target;
const { json: stateJson } = this.state;
const { knob, onChange } = this.props;
@ -65,17 +96,6 @@ class ObjectType extends Component {
}
}
ObjectType.propTypes = {
knob: PropTypes.shape({
name: PropTypes.string,
value: PropTypes.oneOfType([PropTypes.object, PropTypes.array]),
}).isRequired,
onChange: PropTypes.func.isRequired,
};
ObjectType.serialize = object => JSON.stringify(object);
ObjectType.deserialize = value => (value ? JSON.parse(value) : {});
polyfill(ObjectType);
export default ObjectType;

View File

@ -1,85 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import ReactSelect from 'react-select';
import { styled } from '@storybook/theming';
import RadiosType from './Radio';
import CheckboxesType from './Checkboxes';
// TODO: Apply the Storybook theme to react-select
const OptionsSelect = styled(ReactSelect)({
width: '100%',
maxWidth: '300px',
color: 'black',
});
const OptionsType = props => {
const { knob, onChange } = props;
const { display } = knob.optionsObj;
if (display === 'check' || display === 'inline-check') {
const isInline = display === 'inline-check';
return <CheckboxesType {...props} isInline={isInline} />;
}
if (display === 'radio' || display === 'inline-radio') {
const isInline = display === 'inline-radio';
return <RadiosType {...props} isInline={isInline} />;
}
if (display === 'select' || display === 'multi-select') {
const options = Object.keys(knob.options).map(key => ({
value: knob.options[key],
label: key,
}));
const isMulti = display === 'multi-select';
const optionsIndex = options.findIndex(i => i.value === knob.value);
let defaultValue = options[optionsIndex];
let handleChange = e => onChange(e.value);
if (isMulti) {
defaultValue = options.filter(i => knob.value.includes(i.value));
handleChange = values => onChange(values.map(item => item.value));
}
return (
<OptionsSelect
value={defaultValue}
options={options}
isMulti={isMulti}
onChange={handleChange}
/>
);
}
return null;
};
OptionsType.defaultProps = {
knob: {},
display: 'select',
onChange: value => value,
};
OptionsType.propTypes = {
knob: PropTypes.shape({
name: PropTypes.string,
value: PropTypes.oneOfType([PropTypes.array, PropTypes.string]),
options: PropTypes.object,
}),
display: PropTypes.oneOf([
'check',
'inline-check',
'radio',
'inline-radio',
'select',
'multi-select',
]),
onChange: PropTypes.func,
};
OptionsType.serialize = value => value;
OptionsType.deserialize = value => value;
export default OptionsType;

View File

@ -0,0 +1,134 @@
import React, { FunctionComponent } from 'react';
import PropTypes from 'prop-types';
import ReactSelect from 'react-select';
import { ValueType } from 'react-select/lib/types';
import { styled } from '@storybook/theming';
import RadiosType from './Radio';
import CheckboxesType from './Checkboxes';
// TODO: Apply the Storybook theme to react-select
export type OptionsKnobOptionsDisplay =
| 'radio'
| 'inline-radio'
| 'check'
| 'inline-check'
| 'select'
| 'multi-select';
export interface OptionsKnobOptions {
display?: OptionsKnobOptionsDisplay;
}
export interface OptionsTypeKnob<T> {
name: string;
value: T;
defaultValue: T;
options: OptionsTypeOptionsProp<T>;
optionsObj: OptionsKnobOptions;
}
export interface OptionsTypeOptionsProp<T> {
[key: string]: T;
}
export interface OptionsTypeProps<T> {
knob: OptionsTypeKnob<T>;
display: OptionsKnobOptionsDisplay;
onChange: (value: T) => T;
}
// : React.ComponentType<ReactSelectProps>
const OptionsSelect = styled(ReactSelect)({
width: '100%',
maxWidth: '300px',
color: 'black',
});
type ReactSelectOnChangeFn<OptionType = OptionsSelectValueItem> = (
value: ValueType<OptionType>
) => void;
interface OptionsSelectValueItem {
value: any;
label: string;
}
const serialize: { <T>(value: T): T } = value => value;
const deserialize: { <T>(value: T): T } = value => value;
const OptionsType: FunctionComponent<OptionsTypeProps<any>> & {
serialize: typeof serialize;
deserialize: typeof deserialize;
} = props => {
const { knob, onChange } = props;
const { display } = knob.optionsObj;
if (display === 'check' || display === 'inline-check') {
const isInline = display === 'inline-check';
return <CheckboxesType {...props} isInline={isInline} />;
}
if (display === 'radio' || display === 'inline-radio') {
const isInline = display === 'inline-radio';
return <RadiosType {...props} isInline={isInline} />;
}
if (display === 'select' || display === 'multi-select') {
const options: OptionsSelectValueItem[] = Object.keys(knob.options).map(key => ({
value: knob.options[key],
label: key,
}));
const isMulti = display === 'multi-select';
const optionsIndex = options.findIndex(i => i.value === knob.value);
let defaultValue: typeof options | typeof options[0] = options[optionsIndex];
let handleChange: ReactSelectOnChangeFn = (e: OptionsSelectValueItem) => onChange(e.value);
if (isMulti) {
defaultValue = options.filter(i => knob.value.includes(i.value));
handleChange = (values: OptionsSelectValueItem[]) => onChange(values.map(item => item.value));
}
return (
<OptionsSelect
value={defaultValue}
options={options}
isMulti={isMulti}
onChange={handleChange}
/>
);
}
return null;
};
OptionsType.defaultProps = {
knob: {} as any,
display: 'select',
onChange: value => value,
};
OptionsType.propTypes = {
// TODO: remove `any` once DefinitelyTyped/DefinitelyTyped#31280 has been resolved
knob: PropTypes.shape({
name: PropTypes.string,
value: PropTypes.oneOfType([PropTypes.array, PropTypes.string]),
options: PropTypes.object,
}) as any,
display: PropTypes.oneOf<OptionsKnobOptionsDisplay>([
'radio',
'inline-radio',
'check',
'inline-check',
'select',
'multi-select',
]),
onChange: PropTypes.func,
};
OptionsType.serialize = serialize;
OptionsType.deserialize = deserialize;
export default OptionsType;

View File

@ -1,79 +0,0 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { styled } from '@storybook/theming';
const RadiosWrapper = styled.div(({ isInline }) =>
isInline
? {
display: 'flex',
flexWrap: 'wrap',
alignItems: 'center',
'> * + *': {
marginLeft: 10,
},
}
: {}
);
const RadioLabel = styled.label({
padding: '3px 0 3px 5px',
lineHeight: '18px',
display: 'inline-block',
});
class RadiosType extends Component {
renderRadioButtonList({ options }) {
if (Array.isArray(options)) {
return options.map(val => this.renderRadioButton(val, val));
}
return Object.keys(options).map(key => this.renderRadioButton(key, options[key]));
}
renderRadioButton(label, value) {
const opts = { label, value };
const { onChange, knob } = this.props;
const { name } = knob;
const id = `${name}-${opts.value}`;
return (
<div key={id}>
<input
type="radio"
id={id}
name={name}
value={opts.value}
onChange={e => onChange(e.target.value)}
checked={value === knob.value}
/>
<RadioLabel htmlFor={id}>{label}</RadioLabel>
</div>
);
}
render() {
const { knob, isInline } = this.props;
return <RadiosWrapper isInline={isInline}>{this.renderRadioButtonList(knob)}</RadiosWrapper>;
}
}
RadiosType.defaultProps = {
knob: {},
onChange: value => value,
isInline: false,
};
RadiosType.propTypes = {
knob: PropTypes.shape({
name: PropTypes.string,
value: PropTypes.string,
options: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
}),
onChange: PropTypes.func,
isInline: PropTypes.bool,
};
RadiosType.serialize = value => value;
RadiosType.deserialize = value => value;
export default RadiosType;

View File

@ -0,0 +1,104 @@
import React, { Component, WeakValidationMap } from 'react';
import PropTypes from 'prop-types';
import { styled } from '@storybook/theming';
type RadiosTypeKnobValue = string;
export interface RadiosTypeKnob {
name: string;
value: RadiosTypeKnobValue;
defaultValue: RadiosTypeKnobValue;
options: RadiosTypeOptionsProp;
}
export interface RadiosTypeOptionsProp {
[key: string]: RadiosTypeKnobValue;
}
interface RadiosTypeProps {
knob: RadiosTypeKnob;
isInline: boolean;
onChange: (value: RadiosTypeKnobValue) => RadiosTypeKnobValue;
}
interface RadiosWrapperProps {
isInline: boolean;
}
const RadiosWrapper = styled.div(({ isInline }: RadiosWrapperProps) =>
isInline
? {
display: 'flex',
flexWrap: 'wrap',
alignItems: 'center',
'> * + *': {
marginLeft: 10,
},
}
: {}
);
const RadioLabel = styled.label({
padding: '3px 0 3px 5px',
lineHeight: '18px',
display: 'inline-block',
});
class RadiosType extends Component<RadiosTypeProps> {
static defaultProps: RadiosTypeProps = {
knob: {} as any,
onChange: value => value,
isInline: false,
};
static propTypes: WeakValidationMap<RadiosTypeProps> = {
// TODO: remove `any` once DefinitelyTyped/DefinitelyTyped#31280 has been resolved
knob: PropTypes.shape({
name: PropTypes.string,
value: PropTypes.string,
options: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
}) as any,
onChange: PropTypes.func,
isInline: PropTypes.bool,
};
static serialize = (value: RadiosTypeKnobValue) => value;
static deserialize = (value: RadiosTypeKnobValue) => value;
renderRadioButtonList({ options }: RadiosTypeKnob) {
if (Array.isArray(options)) {
return options.map(val => this.renderRadioButton(val, val));
}
return Object.keys(options).map(key => this.renderRadioButton(key, options[key]));
}
renderRadioButton(label: string, value: RadiosTypeKnobValue) {
const opts = { label, value };
const { onChange, knob } = this.props;
const { name } = knob;
const id = `${name}-${opts.value}`;
return (
<div key={id}>
<input
type="radio"
id={id}
name={name}
value={opts.value}
onChange={e => onChange(e.target.value)}
checked={value === knob.value}
/>
<RadioLabel htmlFor={id}>{label}</RadioLabel>
</div>
);
}
render() {
const { knob, isInline } = this.props;
return <RadiosWrapper isInline={isInline}>{this.renderRadioButtonList(knob)}</RadiosWrapper>;
}
}
export default RadiosType;

View File

@ -1,49 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Form } from '@storybook/components';
const SelectType = ({ knob, onChange }) => {
const { options } = knob;
const entries = Array.isArray(options)
? options.reduce((acc, k) => Object.assign(acc, { [k]: k }), {})
: options;
const selectedKey = Object.keys(entries).find(k => entries[k] === knob.value);
return (
<Form.Select
value={selectedKey}
name={knob.name}
onChange={e => {
onChange(entries[e.target.value]);
}}
size="flex"
>
{Object.entries(entries).map(([key]) => (
<option key={key} value={key}>
{key}
</option>
))}
</Form.Select>
);
};
SelectType.defaultProps = {
knob: {},
onChange: value => value,
};
SelectType.propTypes = {
knob: PropTypes.shape({
name: PropTypes.string,
value: PropTypes.any,
options: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
}),
onChange: PropTypes.func,
};
SelectType.serialize = value => value;
SelectType.deserialize = value => value;
export default SelectType;

View File

@ -0,0 +1,73 @@
import React, { FunctionComponent, ChangeEvent } from 'react';
import PropTypes from 'prop-types';
import { Form } from '@storybook/components';
export type SelectTypeKnobValue = string | number | null | undefined;
export interface SelectTypeKnob {
name: string;
value: SelectTypeKnobValue;
options: SelectTypeOptionsProp;
}
export type SelectTypeOptionsProp =
| Record<string, SelectTypeKnobValue>
| NonNullable<SelectTypeKnobValue>[];
export interface SelectTypeProps {
knob: SelectTypeKnob;
onChange: (value: SelectTypeKnobValue) => SelectTypeKnobValue;
}
const serialize = (value: SelectTypeKnobValue) => value;
const deserialize = (value: SelectTypeKnobValue) => value;
const SelectType: FunctionComponent<SelectTypeProps> & {
serialize: typeof serialize;
deserialize: typeof deserialize;
} = ({ knob, onChange }) => {
const { options } = knob;
const entries = Array.isArray(options)
? options.reduce((acc, k) => Object.assign(acc, { [k]: k }), {})
: options;
const selectedKey = Object.keys(entries).find(k => entries[k] === knob.value);
return (
<Form.Select
value={selectedKey}
name={knob.name}
onChange={(e: ChangeEvent<HTMLSelectElement>) => {
onChange(entries[e.target.value]);
}}
size="flex"
>
{Object.entries(entries).map(([key]) => (
<option key={key} value={key}>
{key}
</option>
))}
</Form.Select>
);
};
SelectType.defaultProps = {
knob: {} as any,
onChange: value => value,
};
SelectType.propTypes = {
// TODO: remove `any` once DefinitelyTyped/DefinitelyTyped#31280 has been resolved
knob: PropTypes.shape({
name: PropTypes.string,
value: PropTypes.any,
options: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
}) as any,
onChange: PropTypes.func,
};
SelectType.serialize = serialize;
SelectType.deserialize = deserialize;
export default SelectType;

View File

@ -1,51 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import { Form } from '@storybook/components';
class TextType extends React.Component {
shouldComponentUpdate(nextProps) {
const { knob } = this.props;
return nextProps.knob.value !== knob.value;
}
handleChange = event => {
const { onChange } = this.props;
const { value } = event.target;
onChange(value);
};
render() {
const { knob } = this.props;
return (
<Form.Textarea
id={knob.name}
name={knob.name}
value={knob.value}
onChange={this.handleChange}
size="flex"
/>
);
}
}
TextType.defaultProps = {
knob: {},
onChange: value => value,
};
TextType.propTypes = {
knob: PropTypes.shape({
name: PropTypes.string,
value: PropTypes.string,
}),
onChange: PropTypes.func,
};
TextType.serialize = value => value;
TextType.deserialize = value => value;
export default TextType;

View File

@ -0,0 +1,63 @@
import PropTypes from 'prop-types';
import React, { Component, ChangeEvent, WeakValidationMap } from 'react';
import { Form } from '@storybook/components';
type TextTypeKnobValue = string;
export interface TextTypeKnob {
name: string;
value: TextTypeKnobValue;
}
interface TextTypeProps {
knob: TextTypeKnob;
onChange: (value: TextTypeKnobValue) => TextTypeKnobValue;
}
export default class TextType extends Component<TextTypeProps> {
static defaultProps: TextTypeProps = {
knob: {} as any,
onChange: value => value,
};
static propTypes: WeakValidationMap<TextTypeProps> = {
// TODO: remove `any` once DefinitelyTyped/DefinitelyTyped#31280 has been resolved
knob: PropTypes.shape({
name: PropTypes.string,
value: PropTypes.string,
}) as any,
onChange: PropTypes.func,
};
static serialize = (value: TextTypeKnobValue) => value;
static deserialize = (value: TextTypeKnobValue) => value;
shouldComponentUpdate(nextProps: TextTypeProps) {
const { knob } = this.props;
return nextProps.knob.value !== knob.value;
}
handleChange = (event: ChangeEvent<HTMLTextAreaElement>) => {
const { onChange } = this.props;
const { value } = event.target;
onChange(value);
};
render() {
const { knob } = this.props;
return (
<Form.Textarea
id={knob.name}
name={knob.name}
value={knob.value}
onChange={this.handleChange}
size="flex"
/>
);
}
}

View File

@ -25,3 +25,16 @@ export default {
files: FilesType,
options: OptionsType,
};
export { TextTypeKnob } from './Text';
export { NumberTypeKnob, NumberTypeKnobOptions } from './Number';
export { ColorTypeKnob } from './Color';
export { BooleanTypeKnob } from './Boolean';
export { ObjectTypeKnob } from './Object';
export { SelectTypeKnob, SelectTypeOptionsProp, SelectTypeKnobValue } from './Select';
export { RadiosTypeKnob, RadiosTypeOptionsProp } from './Radio';
export { ArrayTypeKnob } from './Array';
export { DateTypeKnob } from './Date';
export { ButtonTypeKnob, ButtonTypeOnClickProp } from './Button';
export { FileTypeKnob } from './Files';
export { OptionsTypeKnob, OptionsTypeOptionsProp, OptionsKnobOptions } from './Options';

View File

@ -1,21 +1,22 @@
const unconvertable = () => undefined;
const unconvertable = (): undefined => undefined;
export const converters = {
jsonParse: value => JSON.parse(value),
jsonStringify: value => JSON.stringify(value),
simple: value => value,
stringifyIfSet: value => (value === null || value === undefined ? '' : String(value)),
stringifyIfTruthy: value => (value ? String(value) : null),
toArray: value => {
jsonParse: (value: any): any => JSON.parse(value),
jsonStringify: (value: any): string => JSON.stringify(value),
simple: (value: any): any => value,
stringifyIfSet: (value: any): string =>
value === null || value === undefined ? '' : String(value),
stringifyIfTruthy: (value: any): string | null => (value ? String(value) : null),
toArray: (value: any): any[] => {
if (Array.isArray(value)) {
return value;
}
return value.split(',');
},
toBoolean: value => value === 'true',
toDate: value => new Date(value).getTime() || new Date().getTime(),
toFloat: value => (value === '' ? null : parseFloat(value)),
toBoolean: (value: any): boolean => value === 'true',
toDate: (value: any): number => new Date(value).getTime() || new Date().getTime(),
toFloat: (value: any): number => (value === '' ? null : parseFloat(value)),
};
export const serializers = {

Some files were not shown because too many files have changed in this diff Show More