diff --git a/addons/knobs/angular.js b/addons/knobs/angular.js deleted file mode 100644 index c22c26b6732d..000000000000 --- a/addons/knobs/angular.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('./dist/deprecated'); diff --git a/addons/knobs/angular.ts b/addons/knobs/angular.ts new file mode 100644 index 000000000000..f4c9c9e543e8 --- /dev/null +++ b/addons/knobs/angular.ts @@ -0,0 +1 @@ +export * from './dist/deprecated'; diff --git a/addons/knobs/html.js b/addons/knobs/html.js deleted file mode 100644 index c22c26b6732d..000000000000 --- a/addons/knobs/html.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('./dist/deprecated'); diff --git a/addons/knobs/html.ts b/addons/knobs/html.ts new file mode 100644 index 000000000000..f4c9c9e543e8 --- /dev/null +++ b/addons/knobs/html.ts @@ -0,0 +1 @@ +export * from './dist/deprecated'; diff --git a/addons/knobs/marko.js b/addons/knobs/marko.js deleted file mode 100644 index c22c26b6732d..000000000000 --- a/addons/knobs/marko.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('./dist/deprecated'); diff --git a/addons/knobs/marko.ts b/addons/knobs/marko.ts new file mode 100644 index 000000000000..f4c9c9e543e8 --- /dev/null +++ b/addons/knobs/marko.ts @@ -0,0 +1 @@ +export * from './dist/deprecated'; diff --git a/addons/knobs/mithril.js b/addons/knobs/mithril.js deleted file mode 100644 index c22c26b6732d..000000000000 --- a/addons/knobs/mithril.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('./dist/deprecated'); diff --git a/addons/knobs/mithril.ts b/addons/knobs/mithril.ts new file mode 100644 index 000000000000..f4c9c9e543e8 --- /dev/null +++ b/addons/knobs/mithril.ts @@ -0,0 +1 @@ +export * from './dist/deprecated'; diff --git a/addons/knobs/package.json b/addons/knobs/package.json index e7bfa23575fc..af1a21f8bda7 100644 --- a/addons/knobs/package.json +++ b/addons/knobs/package.json @@ -17,7 +17,7 @@ }, "license": "MIT", "main": "dist/index.js", - "jsnext:main": "src/index.js", + "types": "dist/index.d.ts", "scripts": { "prepare": "node ../../scripts/prepare.js" }, @@ -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" } } diff --git a/addons/knobs/polymer.js b/addons/knobs/polymer.js deleted file mode 100644 index c22c26b6732d..000000000000 --- a/addons/knobs/polymer.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('./dist/deprecated'); diff --git a/addons/knobs/polymer.ts b/addons/knobs/polymer.ts new file mode 100644 index 000000000000..f4c9c9e543e8 --- /dev/null +++ b/addons/knobs/polymer.ts @@ -0,0 +1 @@ +export * from './dist/deprecated'; diff --git a/addons/knobs/react.js b/addons/knobs/react.js deleted file mode 100644 index c22c26b6732d..000000000000 --- a/addons/knobs/react.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('./dist/deprecated'); diff --git a/addons/knobs/react.ts b/addons/knobs/react.ts new file mode 100644 index 000000000000..f4c9c9e543e8 --- /dev/null +++ b/addons/knobs/react.ts @@ -0,0 +1 @@ +export * from './dist/deprecated'; diff --git a/addons/knobs/src/KnobManager.js b/addons/knobs/src/KnobManager.ts similarity index 73% rename from addons/knobs/src/KnobManager.js rename to addons/knobs/src/KnobManager.ts index afad9bd3d642..60bbfb0be888 100644 --- a/addons/knobs/src/KnobManager.js +++ b/addons/knobs/src/KnobManager.ts @@ -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 = 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,31 +39,39 @@ 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(); + + channel: Channel; + + options: KnobManagerOptions = {}; + + calling: boolean; - setChannel(channel) { + 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; @@ -77,7 +93,7 @@ export default class KnobManager { return this.getKnobValue(existingKnob); } - const knobInfo = { + const knobInfo: Knob & { name: string; label: string; defaultValue?: any } = { ...options, name: knobName, label: name, diff --git a/addons/knobs/src/KnobStore.js b/addons/knobs/src/KnobStore.js deleted file mode 100644 index 18b5125245a0..000000000000 --- a/addons/knobs/src/KnobStore.js +++ /dev/null @@ -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); - } -} diff --git a/addons/knobs/src/KnobStore.ts b/addons/knobs/src/KnobStore.ts new file mode 100644 index 000000000000..0ba53a7bc58f --- /dev/null +++ b/addons/knobs/src/KnobStore.ts @@ -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 = K & { type: T; groupId?: string }; + +export type Knob = + | KnobPlus<'text', Pick> + | KnobPlus<'boolean', Pick> + | KnobPlus<'number', Pick> + | KnobPlus<'color', Pick> + | KnobPlus<'object', Pick, 'value'>> + | KnobPlus<'select', Pick & { selectV2: true }> + | KnobPlus<'radios', Pick> + | KnobPlus<'array', Pick> + | KnobPlus<'date', Pick> + | KnobPlus<'files', Pick> + | KnobPlus<'button', { value?: unknown; callback: ButtonTypeOnClickProp; hideLabel: true }> + | KnobPlus<'options', Pick, '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 = {}; + + callbacks: Callback[] = []; + + timer: number; + + 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); + } +} diff --git a/addons/knobs/src/components/Panel.js b/addons/knobs/src/components/Panel.tsx similarity index 71% rename from addons/knobs/src/components/Panel.js rename to addons/knobs/src/components/Panel.tsx index a256325f9fea..a5bbb2d8daf6 100644 --- a/addons/knobs/src/components/Panel.js +++ b/addons/knobs/src/components/Panel.tsx @@ -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; +} + +interface KnobPanelOptions { + timestamps?: boolean; +} + +type KnobControlType = ComponentType & { + serialize: (v: any) => any; + deserialize: (v: any) => any; +}; + +export default class KnobPanel extends PureComponent { + 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; + timestamp?: number; + }) => { + const queryParams: Record = {}; 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 = {}; + const groupIds: string[] = []; const knobKeysArray = Object.keys(knobs).filter(key => knobs[key].used); @@ -210,12 +260,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][] => { 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 +301,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, -}; diff --git a/addons/knobs/src/components/PropForm.js b/addons/knobs/src/components/PropForm.js deleted file mode 100644 index aefbb46355bd..000000000000 --- a/addons/knobs/src/components/PropForm.js +++ /dev/null @@ -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 = () => Invalid Type; - -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 ( -
- {knobs.map(knob => { - const changeHandler = this.makeChangeHandler(knob.name, knob.type); - const InputType = TypeMap[knob.type] || InvalidType; - - return ( - - - - ); - })} -
- ); - } -} - -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, -}; diff --git a/addons/knobs/src/components/PropForm.tsx b/addons/knobs/src/components/PropForm.tsx new file mode 100644 index 000000000000..5b4c381151f9 --- /dev/null +++ b/addons/knobs/src/components/PropForm.tsx @@ -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 = () => Invalid Type; + +export default class PropForm extends Component { + static displayName = 'PropForm'; + + static defaultProps = { + knobs: [] as KnobStoreKnob[], + }; + + static propTypes: WeakValidationMap = { + // TODO: remove `any` once DefinitelyTyped/DefinitelyTyped#31280 has been resolved + knobs: PropTypes.arrayOf( + PropTypes.shape({ + name: PropTypes.string, + value: PropTypes.any, + }) + ).isRequired as Requireable, + 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 ( +
+ {knobs.map(knob => { + const changeHandler = this.makeChangeHandler(knob.name, knob.type); + const InputType: ComponentType = TypeMap[knob.type] || InvalidType; + + return ( + + + + ); + })} + + ); + } +} diff --git a/addons/knobs/src/components/types/Array.js b/addons/knobs/src/components/types/Array.js deleted file mode 100644 index 6f198878bd9d..000000000000 --- a/addons/knobs/src/components/types/Array.js +++ /dev/null @@ -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 ( - - ); - } -} - -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; diff --git a/addons/knobs/src/components/types/Array.tsx b/addons/knobs/src/components/types/Array.tsx new file mode 100644 index 000000000000..34140af7940c --- /dev/null +++ b/addons/knobs/src/components/types/Array.tsx @@ -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 { + static defaultProps: Partial = { + knob: {} as any, + onChange: (value: ArrayTypeKnobValue) => value, + }; + + static propTypes: WeakValidationMap = { + // 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) { + 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 ( + + ); + } +} diff --git a/addons/knobs/src/components/types/Boolean.js b/addons/knobs/src/components/types/Boolean.js deleted file mode 100644 index 8264f47fee28..000000000000 --- a/addons/knobs/src/components/types/Boolean.js +++ /dev/null @@ -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 }) => ( - 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; diff --git a/addons/knobs/src/components/types/Boolean.tsx b/addons/knobs/src/components/types/Boolean.tsx new file mode 100644 index 000000000000..d9c1443d260a --- /dev/null +++ b/addons/knobs/src/components/types/Boolean.tsx @@ -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 & { + serialize: typeof serialize; + deserialize: typeof deserialize; +} = ({ knob, onChange }) => ( + 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; diff --git a/addons/knobs/src/components/types/Button.js b/addons/knobs/src/components/types/Button.js deleted file mode 100644 index 39b0a07f5780..000000000000 --- a/addons/knobs/src/components/types/Button.js +++ /dev/null @@ -1,22 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; - -import { Form } from '@storybook/components'; - -const ButtonType = ({ knob, onClick }) => ( - onClick(knob)}> - {knob.name} - -); - -ButtonType.propTypes = { - knob: PropTypes.shape({ - name: PropTypes.string, - }).isRequired, - onClick: PropTypes.func.isRequired, -}; - -ButtonType.serialize = () => undefined; -ButtonType.deserialize = () => undefined; - -export default ButtonType; diff --git a/addons/knobs/src/components/types/Button.tsx b/addons/knobs/src/components/types/Button.tsx new file mode 100644 index 000000000000..4dfe28b66391 --- /dev/null +++ b/addons/knobs/src/components/types/Button.tsx @@ -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 & { + serialize: typeof serialize; + deserialize: typeof deserialize; +} = ({ knob, onClick }) => ( + onClick(knob)}> + {knob.name} + +); + +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, + onClick: PropTypes.func.isRequired, +}; + +ButtonType.serialize = serialize; +ButtonType.deserialize = deserialize; + +export default ButtonType; diff --git a/addons/knobs/src/components/types/Checkboxes.js b/addons/knobs/src/components/types/Checkboxes.js deleted file mode 100644 index a15cf1d64126..000000000000 --- a/addons/knobs/src/components/types/Checkboxes.js +++ /dev/null @@ -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 ( -
- - {label} -
- ); - }; - - render() { - const { knob, isInline } = this.props; - - return ( - - {this.renderCheckboxList(knob)} - - ); - } -} - -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; diff --git a/addons/knobs/src/components/types/Checkboxes.tsx b/addons/knobs/src/components/types/Checkboxes.tsx new file mode 100644 index 000000000000..7b12cf537673 --- /dev/null +++ b/addons/knobs/src/components/types/Checkboxes.tsx @@ -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 { + static defaultProps: CheckboxesTypeProps = { + knob: {} as any, + onChange: value => value, + isInline: false, + }; + + static propTypes: WeakValidationMap = { + // 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) => { + 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 ( +
+ + {label} +
+ ); + }; + + render() { + const { knob, isInline } = this.props; + + return ( + + {this.renderCheckboxList(knob)} + + ); + } +} diff --git a/addons/knobs/src/components/types/Color.js b/addons/knobs/src/components/types/Color.tsx similarity index 58% rename from addons/knobs/src/components/types/Color.js rename to addons/knobs/src/components/types/Color.tsx index 1b440d615616..45121177a256 100644 --- a/addons/knobs/src/components/types/Color.js +++ b/addons/knobs/src/components/types/Color.tsx @@ -1,12 +1,35 @@ 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 }) => ({ @@ -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 { + static propTypes: WeakValidationMap = { + // 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; diff --git a/addons/knobs/src/components/types/Date.js b/addons/knobs/src/components/types/Date.tsx similarity index 66% rename from addons/knobs/src/components/types/Date.js rename to addons/knobs/src/components/types/Date.tsx index f645fcca9e68..e27b70eca10c 100644 --- a/addons/knobs/src/components/types/Date.js +++ b/addons/knobs/src/components/types/Date.tsx @@ -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 { + static defaultProps: DateTypeProps = { + knob: {} as any, + onChange: value => value, + }; + + static propTypes: WeakValidationMap = { + // 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) => { const { knob, onChange } = this.props; const { state } = this; @@ -70,7 +111,7 @@ class DateType extends Component { } }; - onTimeChange = e => { + onTimeChange = (e: ChangeEvent) => { const { knob, onChange } = this.props; const { state } = this; @@ -100,7 +141,7 @@ class DateType extends Component { { + 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; diff --git a/addons/knobs/src/components/types/Files.js b/addons/knobs/src/components/types/Files.js deleted file mode 100644 index 797498d2b935..000000000000 --- a/addons/knobs/src/components/types/Files.js +++ /dev/null @@ -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 }) => ( - 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; diff --git a/addons/knobs/src/components/types/Files.tsx b/addons/knobs/src/components/types/Files.tsx new file mode 100644 index 000000000000..762bdd67d938 --- /dev/null +++ b/addons/knobs/src/components/types/Files.tsx @@ -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(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 & { + serialize: typeof serialize; + deserialize: typeof deserialize; +} = ({ knob, onChange }) => ( + ) => + 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; diff --git a/addons/knobs/src/components/types/Number.js b/addons/knobs/src/components/types/Number.js deleted file mode 100644 index 9a6b677cb8f5..000000000000 --- a/addons/knobs/src/components/types/Number.js +++ /dev/null @@ -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 ? ( - - {knob.min} - - {`${knob.value} / ${knob.max}`} - - ) : ( - - ); - } -} - -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; diff --git a/addons/knobs/src/components/types/Number.tsx b/addons/knobs/src/components/types/Number.tsx new file mode 100644 index 000000000000..686abd9948ee --- /dev/null +++ b/addons/knobs/src/components/types/Number.tsx @@ -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 { + 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) => { + 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 ? ( + + {knob.min} + + {`${knob.value} / ${knob.max}`} + + ) : ( + + ); + } +} diff --git a/addons/knobs/src/components/types/Object.js b/addons/knobs/src/components/types/Object.tsx similarity index 56% rename from addons/knobs/src/components/types/Object.js rename to addons/knobs/src/components/types/Object.tsx index b68e55e24fa5..3000e24cabe3 100644 --- a/addons/knobs/src/components/types/Object.js +++ b/addons/knobs/src/components/types/Object.tsx @@ -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 { + name: string; + value: T; +} + +interface ObjectTypeProps { + knob: ObjectTypeKnob; + onChange: (value: T) => T; +} + +interface ObjectTypeState { + value: string; + failed: boolean; + json?: T; +} + +class ObjectType extends Component> { + 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: { (object: T): string } = object => JSON.stringify(object); + + static deserialize: { (value: string): T } = value => (value ? JSON.parse(value) : {}); + + static getDerivedStateFromProps( + props: ObjectTypeProps, + state: ObjectTypeState + ): ObjectTypeState { if (!deepEqual(props.knob.value, state.json)) { try { return { @@ -26,7 +51,13 @@ class ObjectType extends Component { return null; } - handleChange = e => { + state: ObjectTypeState = { + value: '', + failed: false, + json: {} as any, + }; + + handleChange = (e: ChangeEvent) => { 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; diff --git a/addons/knobs/src/components/types/Options.js b/addons/knobs/src/components/types/Options.js deleted file mode 100644 index 8580f72d7d44..000000000000 --- a/addons/knobs/src/components/types/Options.js +++ /dev/null @@ -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 ; - } - - if (display === 'radio' || display === 'inline-radio') { - const isInline = display === 'inline-radio'; - return ; - } - - 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 ( - - ); - } - 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; diff --git a/addons/knobs/src/components/types/Options.tsx b/addons/knobs/src/components/types/Options.tsx new file mode 100644 index 000000000000..62a972d1a415 --- /dev/null +++ b/addons/knobs/src/components/types/Options.tsx @@ -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 { + name: string; + value: T; + defaultValue: T; + options: OptionsTypeOptionsProp; + optionsObj: OptionsKnobOptions; +} + +export interface OptionsTypeOptionsProp { + [key: string]: T; +} + +export interface OptionsTypeProps { + knob: OptionsTypeKnob; + display: OptionsKnobOptionsDisplay; + onChange: (value: T) => T; +} + +// : React.ComponentType +const OptionsSelect = styled(ReactSelect)({ + width: '100%', + maxWidth: '300px', + color: 'black', +}); + +type ReactSelectOnChangeFn = ( + value: ValueType +) => void; + +interface OptionsSelectValueItem { + value: any; + label: string; +} + +const serialize: { (value: T): T } = value => value; +const deserialize: { (value: T): T } = value => value; + +const OptionsType: FunctionComponent> & { + 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 ; + } + + if (display === 'radio' || display === 'inline-radio') { + const isInline = display === 'inline-radio'; + return ; + } + + 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 ( + + ); + } + + 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([ + 'radio', + 'inline-radio', + 'check', + 'inline-check', + 'select', + 'multi-select', + ]), + onChange: PropTypes.func, +}; + +OptionsType.serialize = serialize; +OptionsType.deserialize = deserialize; + +export default OptionsType; diff --git a/addons/knobs/src/components/types/Radio.js b/addons/knobs/src/components/types/Radio.js deleted file mode 100644 index f4163967e2ea..000000000000 --- a/addons/knobs/src/components/types/Radio.js +++ /dev/null @@ -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 ( -
- onChange(e.target.value)} - checked={value === knob.value} - /> - {label} -
- ); - } - - render() { - const { knob, isInline } = this.props; - - return {this.renderRadioButtonList(knob)}; - } -} - -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; diff --git a/addons/knobs/src/components/types/Radio.tsx b/addons/knobs/src/components/types/Radio.tsx new file mode 100644 index 000000000000..9cd910f67959 --- /dev/null +++ b/addons/knobs/src/components/types/Radio.tsx @@ -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 { + static defaultProps: RadiosTypeProps = { + knob: {} as any, + onChange: value => value, + isInline: false, + }; + + static propTypes: WeakValidationMap = { + // 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 ( +
+ onChange(e.target.value)} + checked={value === knob.value} + /> + {label} +
+ ); + } + + render() { + const { knob, isInline } = this.props; + + return {this.renderRadioButtonList(knob)}; + } +} + +export default RadiosType; diff --git a/addons/knobs/src/components/types/Select.js b/addons/knobs/src/components/types/Select.js deleted file mode 100644 index ff3809505ff5..000000000000 --- a/addons/knobs/src/components/types/Select.js +++ /dev/null @@ -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 ( - { - onChange(entries[e.target.value]); - }} - size="flex" - > - {Object.entries(entries).map(([key]) => ( - - ))} - - ); -}; - -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; diff --git a/addons/knobs/src/components/types/Select.tsx b/addons/knobs/src/components/types/Select.tsx new file mode 100644 index 000000000000..e650527c0172 --- /dev/null +++ b/addons/knobs/src/components/types/Select.tsx @@ -0,0 +1,73 @@ +import React, { FunctionComponent, ChangeEvent } from 'react'; +import PropTypes from 'prop-types'; + +import { Form } from '@storybook/components'; + +type SelectTypeKnobValue = string; + +export interface SelectTypeKnob { + name: string; + value: SelectTypeKnobValue; + options: SelectTypeOptionsProp; +} + +export interface SelectTypeOptionsProp { + [key: string]: SelectTypeKnobValue; +} + +export interface SelectTypeProps { + knob: SelectTypeKnob; + onChange: (value: SelectTypeKnobValue) => SelectTypeKnobValue; +} + +const serialize = (value: SelectTypeKnobValue) => value; +const deserialize = (value: SelectTypeKnobValue) => value; + +const SelectType: FunctionComponent & { + 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 ( + ) => { + onChange(entries[e.target.value]); + }} + size="flex" + > + {Object.entries(entries).map(([key]) => ( + + ))} + + ); +}; + +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; diff --git a/addons/knobs/src/components/types/Text.js b/addons/knobs/src/components/types/Text.js deleted file mode 100644 index 2f95e8f5b0f0..000000000000 --- a/addons/knobs/src/components/types/Text.js +++ /dev/null @@ -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 ( - - ); - } -} - -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; diff --git a/addons/knobs/src/components/types/Text.tsx b/addons/knobs/src/components/types/Text.tsx new file mode 100644 index 000000000000..cf939ffa3f1e --- /dev/null +++ b/addons/knobs/src/components/types/Text.tsx @@ -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 { + static defaultProps: TextTypeProps = { + knob: {} as any, + onChange: value => value, + }; + + static propTypes: WeakValidationMap = { + // 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) => { + const { onChange } = this.props; + const { value } = event.target; + + onChange(value); + }; + + render() { + const { knob } = this.props; + + return ( + + ); + } +} diff --git a/addons/knobs/src/components/types/index.js b/addons/knobs/src/components/types/index.ts similarity index 51% rename from addons/knobs/src/components/types/index.js rename to addons/knobs/src/components/types/index.ts index b0c620dd94ca..c8a96d5245f0 100644 --- a/addons/knobs/src/components/types/index.js +++ b/addons/knobs/src/components/types/index.ts @@ -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 } 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'; diff --git a/addons/knobs/src/converters.js b/addons/knobs/src/converters.ts similarity index 59% rename from addons/knobs/src/converters.js rename to addons/knobs/src/converters.ts index 7e63c8de78a3..0c07813bce0b 100644 --- a/addons/knobs/src/converters.js +++ b/addons/knobs/src/converters.ts @@ -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 = { diff --git a/addons/knobs/src/deprecated.js b/addons/knobs/src/deprecated.ts similarity index 100% rename from addons/knobs/src/deprecated.js rename to addons/knobs/src/deprecated.ts diff --git a/addons/knobs/src/index.js b/addons/knobs/src/index.ts similarity index 59% rename from addons/knobs/src/index.js rename to addons/knobs/src/index.ts index 7035ead6c55f..4b99546a1bb8 100644 --- a/addons/knobs/src/index.js +++ b/addons/knobs/src/index.ts @@ -1,22 +1,34 @@ import addons, { makeDecorator } from '@storybook/addons'; import { SET_OPTIONS } from './shared'; - import { manager, registerKnobs } from './registerKnobs'; - -export function knob(name, optionsParam) { +import { + NumberTypeKnobOptions, + ButtonTypeOnClickProp, + RadiosTypeOptionsProp, + SelectTypeOptionsProp, + OptionsTypeOptionsProp, + OptionsKnobOptions, +} from './components/types'; + +export function knob(name: string, optionsParam: any) { return manager.knob(name, optionsParam); } -export function text(name, value, groupId) { +export function text(name: string, value: string, groupId?: string) { return manager.knob(name, { type: 'text', value, groupId }); } -export function boolean(name, value, groupId) { +export function boolean(name: string, value: boolean, groupId?: string) { return manager.knob(name, { type: 'boolean', value, groupId }); } -export function number(name, value, options = {}, groupId) { +export function number( + name: string, + value: number, + options: NumberTypeKnobOptions = {}, + groupId?: string +) { const rangeDefaults = { min: 0, max: 10, @@ -31,8 +43,8 @@ export function number(name, value, options = {}, groupId) { : options; const finalOptions = { + type: 'number' as 'number', ...mergedOptions, - type: 'number', value, groupId, }; @@ -40,40 +52,56 @@ export function number(name, value, options = {}, groupId) { return manager.knob(name, finalOptions); } -export function color(name, value, groupId) { +export function color(name: string, value: string, groupId?: string) { return manager.knob(name, { type: 'color', value, groupId }); } -export function object(name, value, groupId) { +export function object(name: string, value: T, groupId?: string) { return manager.knob(name, { type: 'object', value, groupId }); } -export function select(name, options, value, groupId) { +export function select( + name: string, + options: SelectTypeOptionsProp, + value: string, + groupId?: string +) { return manager.knob(name, { type: 'select', selectV2: true, options, value, groupId }); } -export function radios(name, options, value, groupId) { +export function radios( + name: string, + options: RadiosTypeOptionsProp, + value: string, + groupId?: string +) { return manager.knob(name, { type: 'radios', options, value, groupId }); } -export function array(name, value, separator = ',', groupId) { +export function array(name: string, value: string[], separator = ',', groupId?: string) { return manager.knob(name, { type: 'array', value, separator, groupId }); } -export function date(name, value = new Date(), groupId) { +export function date(name: string, value = new Date(), groupId?: string) { const proxyValue = value ? value.getTime() : null; return manager.knob(name, { type: 'date', value: proxyValue, groupId }); } -export function button(name, callback, groupId) { +export function button(name: string, callback: ButtonTypeOnClickProp, groupId?: string) { return manager.knob(name, { type: 'button', callback, hideLabel: true, groupId }); } -export function files(name, accept, value = [], groupId) { +export function files(name: string, accept: string, value: string[] = [], groupId?: string) { return manager.knob(name, { type: 'files', accept, value, groupId }); } -export function optionsKnob(name, valuesObj, value, optionsObj, groupId) { +export function optionsKnob( + name: string, + valuesObj: OptionsTypeOptionsProp, + value: string, + optionsObj: OptionsKnobOptions, + groupId?: string +) { return manager.knob(name, { type: 'options', options: valuesObj, value, optionsObj, groupId }); } diff --git a/addons/knobs/src/register.js b/addons/knobs/src/register.tsx similarity index 88% rename from addons/knobs/src/register.js rename to addons/knobs/src/register.tsx index 2808815b3958..a6753c557a9d 100644 --- a/addons/knobs/src/register.js +++ b/addons/knobs/src/register.tsx @@ -6,7 +6,6 @@ import { ADDON_ID, PANEL_ID, PARAM_KEY } from './shared'; addons.register(ADDON_ID, api => { addons.addPanel(PANEL_ID, { title: 'Knobs', - // eslint-disable-next-line react/prop-types render: ({ active, key }) => , paramKey: PARAM_KEY, }); diff --git a/addons/knobs/src/registerKnobs.js b/addons/knobs/src/registerKnobs.ts similarity index 91% rename from addons/knobs/src/registerKnobs.js rename to addons/knobs/src/registerKnobs.ts index 07e3ecf08d73..866c47ac2ed0 100644 --- a/addons/knobs/src/registerKnobs.js +++ b/addons/knobs/src/registerKnobs.ts @@ -4,6 +4,7 @@ import debounce from 'lodash/debounce'; import KnobManager from './KnobManager'; import { CHANGE, CLICK, RESET, SET } from './shared'; +import { KnobStoreKnob } from './KnobStore'; export const manager = new KnobManager(); const { knobStore } = manager; @@ -13,7 +14,7 @@ function forceReRender() { addons.getChannel().emit(FORCE_RE_RENDER); } -function setPaneKnobs(timestamp = +new Date()) { +function setPaneKnobs(timestamp: boolean | number = +new Date()) { const channel = addons.getChannel(); channel.emit(SET, { knobs: knobStore.getAll(), timestamp }); } @@ -29,7 +30,7 @@ const debouncedResetAndForceUpdate = debounce( COMPONENT_FORCE_RENDER_DEBOUNCE_DELAY_MS ); -function knobChanged(change) { +function knobChanged(change: KnobStoreKnob) { const { name } = change; const { value } = change; // Update the related knob and it's value. const knobOptions = knobStore.get(name); @@ -42,7 +43,7 @@ function knobChanged(change) { } } -function knobClicked(clicked) { +function knobClicked(clicked: KnobStoreKnob) { const knobOptions = knobStore.get(clicked.name); if (knobOptions.callback() !== false) { forceReRender(); diff --git a/addons/knobs/src/shared.js b/addons/knobs/src/shared.ts similarity index 100% rename from addons/knobs/src/shared.js rename to addons/knobs/src/shared.ts diff --git a/addons/knobs/src/typings.d.ts b/addons/knobs/src/typings.d.ts new file mode 100644 index 000000000000..b0b788b35a7e --- /dev/null +++ b/addons/knobs/src/typings.d.ts @@ -0,0 +1,2 @@ +declare module 'global'; +declare module '@storybook/client-api'; diff --git a/addons/knobs/tsconfig.json b/addons/knobs/tsconfig.json new file mode 100644 index 000000000000..82ce44329cc6 --- /dev/null +++ b/addons/knobs/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "./src", + "types": ["webpack-env"] + }, + "include": ["src/**/*"], + "exclude": ["src/__tests__/**/*"] +} diff --git a/addons/knobs/vue.js b/addons/knobs/vue.js deleted file mode 100644 index c22c26b6732d..000000000000 --- a/addons/knobs/vue.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('./dist/deprecated'); diff --git a/addons/knobs/vue.ts b/addons/knobs/vue.ts new file mode 100644 index 000000000000..f4c9c9e543e8 --- /dev/null +++ b/addons/knobs/vue.ts @@ -0,0 +1 @@ +export * from './dist/deprecated'; diff --git a/docs/yarn.lock b/docs/yarn.lock index d29449f476bc..5a6d19e1b3a6 100644 --- a/docs/yarn.lock +++ b/docs/yarn.lock @@ -261,27 +261,20 @@ dependencies: "@ndhoule/map" "^2.0.1" -"@storybook/client-logger@5.2.0-alpha.34": - version "5.2.0-alpha.34" - resolved "https://registry.yarnpkg.com/@storybook/client-logger/-/client-logger-5.2.0-alpha.34.tgz#9de93faf736fb58b1f3bd04b500f40dc6231e3d9" - integrity sha512-ukvCH+Fudey0RagrbxbVCy/ZwWfScVDFjGe8yieFCXVLRyfMERuEWgz/WBrgwFIVhzGt4JQgewolv27QLDysNg== +"@storybook/client-logger@5.2.0-alpha.36": + version "5.2.0-alpha.36" + resolved "https://registry.yarnpkg.com/@storybook/client-logger/-/client-logger-5.2.0-alpha.36.tgz#aff65cf246982176803e751c418d0052f507aa20" + integrity sha512-5gTBxaASFAgXh12m5GX0cvbSrIJWXhfcOuXSD48g6PuHHRup9PaoM9PNwgnB1HolJFYl8CG6jC5I/hnaBQr/QQ== dependencies: core-js "^3.0.1" -"@storybook/client-logger@5.2.0-alpha.35": - version "5.2.0-alpha.35" - resolved "https://registry.yarnpkg.com/@storybook/client-logger/-/client-logger-5.2.0-alpha.35.tgz#73ddcf2bfa5885407da580b0ffac2bcbe904e260" - integrity sha512-/G/nmW2k4H45nl/cghbmfXraixkFAJd0NrZ5Ch9ZsCuZYYsvMLfasF6Y4FQYykzadMCsdlrpK1dVK2P9YqhyNQ== +"@storybook/components@5.2.0-alpha.36": + version "5.2.0-alpha.36" + resolved "https://registry.yarnpkg.com/@storybook/components/-/components-5.2.0-alpha.36.tgz#370e3e593923ce6e5698d560156bd84c4a3c78cd" + integrity sha512-H/Z9wLV/ZJH73Ko1R3Anw4oVodf4z/hcQP0UK3XRIOejv1iI/2ZoRWyIkD/b5IoouHPIXlWkplTkLp2eCFDvPA== dependencies: - core-js "^3.0.1" - -"@storybook/components@5.2.0-alpha.34": - version "5.2.0-alpha.34" - resolved "https://registry.yarnpkg.com/@storybook/components/-/components-5.2.0-alpha.34.tgz#397f146419d6570661d50db6e5638b98803a4b30" - integrity sha512-LStRAgiKSlIouZiK2xtSF430gg42CHD/gwpncdJYQFLY8mWr46Ms0Fzk5YWbXi/xrjFnuX6aQxl7n28CjPmoqg== - dependencies: - "@storybook/client-logger" "5.2.0-alpha.34" - "@storybook/theming" "5.2.0-alpha.34" + "@storybook/client-logger" "5.2.0-alpha.36" + "@storybook/theming" "5.2.0-alpha.36" core-js "^3.0.1" global "^4.3.2" markdown-to-jsx "^6.9.1" @@ -299,32 +292,14 @@ recompose "^0.30.0" simplebar-react "^1.0.0-alpha.6" -"@storybook/theming@5.2.0-alpha.34": - version "5.2.0-alpha.34" - resolved "https://registry.yarnpkg.com/@storybook/theming/-/theming-5.2.0-alpha.34.tgz#fa65fa34af6231a5332e2bebd0f4174023f9bb4d" - integrity sha512-F900cTk2JkwrghPr5wFZ95zhAb+Ygp8zKp9MGe0aL4c/2OJrKrNhhA7nvtskdcLg2jJoBXSkwaRqe9rxtzND/g== - dependencies: - "@emotion/core" "^10.0.9" - "@emotion/styled" "^10.0.7" - "@storybook/client-logger" "5.2.0-alpha.34" - common-tags "^1.8.0" - core-js "^3.0.1" - deep-object-diff "^1.1.0" - emotion-theming "^10.0.9" - global "^4.3.2" - memoizerific "^1.11.3" - polished "^3.3.1" - prop-types "^15.7.2" - resolve-from "^5.0.0" - -"@storybook/theming@5.2.0-alpha.35": - version "5.2.0-alpha.35" - resolved "https://registry.yarnpkg.com/@storybook/theming/-/theming-5.2.0-alpha.35.tgz#6d97548b7c31e7b50f4e28acbbae8b04cef26111" - integrity sha512-s+6b3BvxPs+LLsU2JIGDEId5+WNd+xkTzyKZAmf5/aS9e6j0p3tSJFiB+7gObt256yyo/SPe9Ke2szQdPfTIUg== +"@storybook/theming@5.2.0-alpha.36": + version "5.2.0-alpha.36" + resolved "https://registry.yarnpkg.com/@storybook/theming/-/theming-5.2.0-alpha.36.tgz#52c963d9697740762ef1f5e14edae30885a8fce5" + integrity sha512-nDLK6j0rR2lmNuOsvySuv2QsCbLf+u/CWsSHcIAajGIQWyVqoXAHf/bbDOPNxUPIWMj9noZ2zOxakVQ69Ksl3w== dependencies: "@emotion/core" "^10.0.9" "@emotion/styled" "^10.0.7" - "@storybook/client-logger" "5.2.0-alpha.35" + "@storybook/client-logger" "5.2.0-alpha.36" common-tags "^1.8.0" core-js "^3.0.1" deep-object-diff "^1.1.0" diff --git a/yarn.lock b/yarn.lock index 326e5632faba..7d330f657653 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4197,6 +4197,11 @@ "@types/cheerio" "*" "@types/react" "*" +"@types/escape-html@0.0.20": + version "0.0.20" + resolved "https://registry.yarnpkg.com/@types/escape-html/-/escape-html-0.0.20.tgz#cae698714dd61ebee5ab3f2aeb9a34ba1011735a" + integrity sha512-6dhZJLbA7aOwkYB2GDGdIqJ20wmHnkDzaxV9PJXe7O02I2dSFTERzRB6JrX6cWKaS+VqhhY7cQUMCbO5kloFUw== + "@types/estree@*", "@types/estree@0.0.39": version "0.0.39" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f" @@ -4368,13 +4373,27 @@ "@types/history" "*" "@types/react" "*" -"@types/react-dom@^16.8.2": +"@types/react-color@^3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/react-color/-/react-color-3.0.1.tgz#5433e2f503ea0e0831cbc6fd0c20f8157d93add0" + integrity sha512-J6mYm43Sid9y+OjZ7NDfJ2VVkeeuTPNVImNFITgQNXodHteKfl/t/5pAR5Z9buodZ2tCctsZjgiMlQOpfntakw== + dependencies: + "@types/react" "*" + +"@types/react-dom@*", "@types/react-dom@^16.8.2": version "16.8.4" resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.8.4.tgz#7fb7ba368857c7aa0f4e4511c4710ca2c5a12a88" integrity sha512-eIRpEW73DCzPIMaNBDP5pPIpK1KXyZwNgfxiVagb5iGiz6da+9A5hslSX6GAQKdO7SayVCS/Fr2kjqprgAvkfA== dependencies: "@types/react" "*" +"@types/react-lifecycles-compat@^3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/react-lifecycles-compat/-/react-lifecycles-compat-3.0.1.tgz#a0b1fe18cfb9435bd52737829a69cbe93faf32e2" + integrity sha512-4KiU5s1Go4xRbf7t6VxUUpBeN5PGjpjpBv9VvET4uiPHC500VNYBclU13f8ehHkHoZL39b2cfwHu6RzbV3b44A== + dependencies: + "@types/react" "*" + "@types/react-native@^0.57.57": version "0.57.60" resolved "https://registry.yarnpkg.com/@types/react-native/-/react-native-0.57.60.tgz#61e97a84e2f64ed971e7d238bb30cec188898235" @@ -4393,6 +4412,15 @@ hoist-non-react-statics "^3.3.0" redux "^4.0.0" +"@types/react-select@^2.0.19": + version "2.0.19" + resolved "https://registry.yarnpkg.com/@types/react-select/-/react-select-2.0.19.tgz#59a80ef81a4a5cb37f59970c53a4894d15065199" + integrity sha512-5GGBO3npQ0G/poQmEn+kI3Vn3DoJ9WjRXCeGcpwLxd5rYmjYPH235lbYPX5aclXE2RqEXyFxd96oh0wYwPXYpg== + dependencies: + "@types/react" "*" + "@types/react-dom" "*" + "@types/react-transition-group" "*" + "@types/react-syntax-highlighter@10.1.0": version "10.1.0" resolved "https://registry.yarnpkg.com/@types/react-syntax-highlighter/-/react-syntax-highlighter-10.1.0.tgz#9c534e29bbe05dba9beae1234f3ae944836685d4" @@ -4407,6 +4435,13 @@ dependencies: "@types/react" "*" +"@types/react-transition-group@*": + version "2.9.2" + resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-2.9.2.tgz#c48cf2a11977c8b4ff539a1c91d259eaa627028d" + integrity sha512-5Fv2DQNO+GpdPZcxp2x/OQG/H19A01WlmpjVD9cKvVFmoVLOZ9LvBgSWG6pSXIU4og5fgbvGPaCV5+VGkWAEHA== + dependencies: + "@types/react" "*" + "@types/react@*", "@types/react@^16.8.14", "@types/react@^16.8.3": version "16.8.18" resolved "https://registry.yarnpkg.com/@types/react/-/react-16.8.18.tgz#fe66fb748b0b6ca9709d38b87b2d1356d960a511"