mirror of
https://github.com/storybookjs/storybook.git
synced 2025-04-05 06:21:15 +08:00
Merge branch 'next' into setup-travis
# Conflicts: # yarn.lock
This commit is contained in:
commit
6bcdbf209a
68
CHANGELOG.md
68
CHANGELOG.md
@ -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)
|
||||
|
||||
|
132
MIGRATION.md
132
MIGRATION.md
@ -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
|
||||
|
@ -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`
|
||||
|
||||
|
@ -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",
|
||||
|
@ -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,
|
||||
}));
|
||||
|
||||
|
@ -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',
|
||||
|
@ -31,7 +31,7 @@ enum CheckBoxStates {
|
||||
INDETERMINATE,
|
||||
}
|
||||
|
||||
const Checkbox = styled.input(({ disabled }) => ({
|
||||
const Checkbox = styled.input<{ disabled: boolean }>(({ disabled }) => ({
|
||||
cursor: disabled ? 'not-allowed' : 'pointer',
|
||||
}));
|
||||
|
||||
|
@ -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',
|
||||
|
@ -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}`,
|
||||
|
@ -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>
|
||||
|
@ -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',
|
||||
|
@ -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, {
|
||||
|
@ -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",
|
||||
|
@ -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,
|
||||
|
@ -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`;
|
||||
|
@ -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,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -2,4 +2,5 @@ export interface ActionOptions {
|
||||
depth?: number;
|
||||
clearOnStoryChange?: boolean;
|
||||
limit?: number;
|
||||
allowFunction?: boolean;
|
||||
}
|
||||
|
81
addons/actions/src/models/ActionsFunction.ts
Normal file
81
addons/actions/src/models/ActionsFunction.ts
Normal 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>;
|
||||
}
|
@ -1,5 +1,3 @@
|
||||
import { HandlerFunction } from './HandlerFunction';
|
||||
|
||||
export interface ActionsMap {
|
||||
[key: string]: HandlerFunction;
|
||||
}
|
||||
export type ActionsMap<T extends string = string> = Record<T, HandlerFunction>;
|
||||
|
@ -1,4 +1,5 @@
|
||||
export * from './ActionDisplay';
|
||||
export * from './ActionsFunction';
|
||||
export * from './ActionOptions';
|
||||
export * from './ActionsMap';
|
||||
export * from './DecoratorFunction';
|
||||
|
@ -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),
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
@ -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",
|
||||
|
10
addons/centered/angular.d.ts
vendored
10
addons/centered/angular.d.ts
vendored
@ -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;
|
||||
}
|
||||
|
@ -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": "*"
|
||||
|
@ -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 {
|
||||
|
@ -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": "*"
|
||||
|
@ -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"
|
||||
|
@ -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,
|
||||
});
|
||||
});
|
||||
|
@ -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",
|
||||
|
@ -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
1
addons/docs/angular/index.js
vendored
Normal file
@ -0,0 +1 @@
|
||||
module.exports = require('../common/index');
|
1
addons/docs/angular/preset.js
vendored
Normal file
1
addons/docs/angular/preset.js
vendored
Normal file
@ -0,0 +1 @@
|
||||
module.exports = require('../common/preset');
|
@ -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",
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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",
|
||||
|
@ -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>
|
@ -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);
|
||||
};
|
||||
|
@ -1,3 +1,5 @@
|
||||
export const PARAM_KEY = 'events';
|
||||
|
||||
export const ADDON_ID = 'storybook/events';
|
||||
|
||||
export const PANEL_ID = `${ADDON_ID}/panel`;
|
@ -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();
|
||||
};
|
@ -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
2
addons/events/src/typings.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
declare module 'react-lifecycles-compat';
|
||||
declare module 'format-json';
|
13
addons/events/tsconfig.json
Normal file
13
addons/events/tsconfig.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"types": ["webpack-env"]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"src/__tests__/**/*"
|
||||
]
|
||||
}
|
@ -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"
|
||||
|
@ -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",
|
||||
|
@ -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,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
|
@ -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 };
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
});
|
||||
});
|
||||
|
@ -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`;
|
||||
|
||||
|
@ -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.
|
||||
|
1
addons/knobs/angular.js
vendored
1
addons/knobs/angular.js
vendored
@ -1 +0,0 @@
|
||||
module.exports = require('./dist/deprecated');
|
1
addons/knobs/angular.ts
Normal file
1
addons/knobs/angular.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './dist/deprecated';
|
@ -1 +0,0 @@
|
||||
module.exports = require('./dist/deprecated');
|
1
addons/knobs/html.ts
Normal file
1
addons/knobs/html.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './dist/deprecated';
|
@ -1 +0,0 @@
|
||||
module.exports = require('./dist/deprecated');
|
1
addons/knobs/marko.ts
Normal file
1
addons/knobs/marko.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './dist/deprecated';
|
@ -1 +0,0 @@
|
||||
module.exports = require('./dist/deprecated');
|
1
addons/knobs/mithril.ts
Normal file
1
addons/knobs/mithril.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './dist/deprecated';
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -1 +0,0 @@
|
||||
module.exports = require('./dist/deprecated');
|
1
addons/knobs/polymer.ts
Normal file
1
addons/knobs/polymer.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './dist/deprecated';
|
1
addons/knobs/react.js
vendored
1
addons/knobs/react.js
vendored
@ -1 +0,0 @@
|
||||
module.exports = require('./dist/deprecated');
|
1
addons/knobs/react.ts
Normal file
1
addons/knobs/react.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './dist/deprecated';
|
@ -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,
|
||||
};
|
||||
|
||||
|
@ -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() {
|
@ -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);
|
||||
}
|
||||
}
|
101
addons/knobs/src/KnobStore.ts
Normal file
101
addons/knobs/src/KnobStore.ts
Normal 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);
|
||||
}
|
||||
}
|
@ -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,
|
||||
};
|
@ -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,
|
||||
};
|
62
addons/knobs/src/components/PropForm.tsx
Normal file
62
addons/knobs/src/components/PropForm.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
@ -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;
|
80
addons/knobs/src/components/types/Array.tsx
Normal file
80
addons/knobs/src/components/types/Array.tsx
Normal 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"
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
@ -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;
|
63
addons/knobs/src/components/types/Boolean.tsx
Normal file
63
addons/knobs/src/components/types/Boolean.tsx
Normal 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;
|
@ -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;
|
45
addons/knobs/src/components/types/Button.tsx
Normal file
45
addons/knobs/src/components/types/Button.tsx
Normal 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;
|
@ -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;
|
135
addons/knobs/src/components/types/Checkboxes.tsx
Normal file
135
addons/knobs/src/components/types/Checkboxes.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
@ -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;
|
@ -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;
|
@ -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;
|
68
addons/knobs/src/components/types/Files.tsx
Normal file
68
addons/knobs/src/components/types/Files.tsx
Normal 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;
|
@ -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;
|
123
addons/knobs/src/components/types/Number.tsx
Normal file
123
addons/knobs/src/components/types/Number.tsx
Normal 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"
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
@ -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;
|
@ -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;
|
134
addons/knobs/src/components/types/Options.tsx
Normal file
134
addons/knobs/src/components/types/Options.tsx
Normal 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;
|
@ -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;
|
104
addons/knobs/src/components/types/Radio.tsx
Normal file
104
addons/knobs/src/components/types/Radio.tsx
Normal 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;
|
@ -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;
|
73
addons/knobs/src/components/types/Select.tsx
Normal file
73
addons/knobs/src/components/types/Select.tsx
Normal 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;
|
@ -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;
|
63
addons/knobs/src/components/types/Text.tsx
Normal file
63
addons/knobs/src/components/types/Text.tsx
Normal 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"
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
@ -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';
|
@ -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
Loading…
x
Reference in New Issue
Block a user