diff --git a/addons/knobs/package.json b/addons/knobs/package.json index 1347db62a43d..cdcce847e030 100644 --- a/addons/knobs/package.json +++ b/addons/knobs/package.json @@ -46,6 +46,7 @@ "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/src/KnobManager.ts b/addons/knobs/src/KnobManager.ts index dc76bd5ef47d..701252c7c3d6 100644 --- a/addons/knobs/src/KnobManager.ts +++ b/addons/knobs/src/KnobManager.ts @@ -1,25 +1,36 @@ /* eslint no-underscore-dangle: 0 */ + +// @ts-ignore import { navigator } from 'global'; import escape from 'escape-html'; - +// TODO: remove ts-ignore once client-api is typed +// @ts-ignore 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, KnobStoreKnob } 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 +42,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 { knobStore } = this; @@ -75,7 +94,7 @@ export default class KnobManager { return this.getKnobValue(existingKnob); } - const knobInfo = { + const knobInfo: Knob & { name: string; defaultValue?: any } = { ...options, name, }; diff --git a/addons/knobs/src/KnobStore.ts b/addons/knobs/src/KnobStore.ts index 18b5125245a0..4abdb288bf01 100644 --- a/addons/knobs/src/KnobStore.ts +++ b/addons/knobs/src/KnobStore.ts @@ -1,20 +1,64 @@ -const callArg = fn => fn(); -const callAll = fns => fns.forEach(callArg); +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; + used?: boolean; + defaultValue?: any; + hideLabel?: boolean; + callback?: () => any; +}; + +const callArg = (fn: Callback) => fn(); +const callAll = (fns: Callback[]) => fns.forEach(callArg); export default class KnobStore { - constructor() { - this.store = {}; - this.callbacks = []; - } + store: Record = {}; + + callbacks: Callback[] = []; + + timer: number; - has(key) { + has(key: string) { return this.store[key] !== undefined; } - set(key, value) { - this.store[key] = value; - this.store[key].used = true; - this.store[key].groupId = value.groupId; + 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) { @@ -23,7 +67,7 @@ export default class KnobStore { this.timer = setTimeout(callAll, 50, this.callbacks); } - get(key) { + get(key: string) { const knob = this.store[key]; if (knob) { knob.used = true; @@ -45,11 +89,11 @@ export default class KnobStore { }); } - subscribe(cb) { + subscribe(cb: Callback) { this.callbacks.push(cb); } - unsubscribe(cb) { + unsubscribe(cb: Callback) { const index = this.callbacks.indexOf(cb); this.callbacks.splice(index, 1); } diff --git a/addons/knobs/src/components/Panel.tsx b/addons/knobs/src/components/Panel.tsx index a256325f9fea..aae80957d7be 100644 --- a/addons/knobs/src/components/Panel.tsx +++ b/addons/knobs/src/components/Panel.tsx @@ -1,6 +1,7 @@ -import React, { PureComponent, Fragment } from 'react'; +import React, { PureComponent, Fragment, ComponentType } from 'react'; import PropTypes from 'prop-types'; import qs from 'qs'; +// @ts-ignore import { document } from 'global'; import { styled } from '@storybook/theming'; import copy from 'copy-to-clipboard'; @@ -18,6 +19,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 +34,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 +111,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 +134,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 +162,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 +170,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 +190,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 +214,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 +261,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 +302,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.tsx b/addons/knobs/src/components/PropForm.tsx index 05df92a1e273..c8d8ebcecbd4 100644 --- a/addons/knobs/src/components/PropForm.tsx +++ b/addons/knobs/src/components/PropForm.tsx @@ -1,13 +1,38 @@ -import React, { Component } from 'react'; +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 { - makeChangeHandler(name, 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 }; @@ -23,7 +48,7 @@ export default class PropForm extends Component {
{knobs.map(knob => { const changeHandler = this.makeChangeHandler(knob.name, knob.type); - const InputType = TypeMap[knob.type] || InvalidType; + const InputType: ComponentType = TypeMap[knob.type] || InvalidType; return ( @@ -35,16 +60,3 @@ export default class PropForm extends Component { ); } } - -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/types/Array.tsx b/addons/knobs/src/components/types/Array.tsx index a0a1fb81e02d..34140af7940c 100644 --- a/addons/knobs/src/components/types/Array.tsx +++ b/addons/knobs/src/components/types/Array.tsx @@ -5,12 +5,14 @@ import { Form } from '@storybook/components'; type ArrayTypeKnobValue = string[]; +export interface ArrayTypeKnob { + name: string; + value: ArrayTypeKnobValue; + separator: string; +} + interface ArrayTypeProps { - knob: { - name: string; - value: ArrayTypeKnobValue; - separator: string; - }; + knob: ArrayTypeKnob; onChange: (value: ArrayTypeKnobValue) => ArrayTypeKnobValue; } diff --git a/addons/knobs/src/components/types/Boolean.tsx b/addons/knobs/src/components/types/Boolean.tsx index 818aa5ef1084..d9c1443d260a 100644 --- a/addons/knobs/src/components/types/Boolean.tsx +++ b/addons/knobs/src/components/types/Boolean.tsx @@ -5,12 +5,14 @@ import { styled } from '@storybook/theming'; type BooleanTypeKnobValue = boolean; -interface BooleanTypeProps { - knob: { - name: string; - value: BooleanTypeKnobValue; - separator: string; - }; +export interface BooleanTypeKnob { + name: string; + value: BooleanTypeKnobValue; + separator: string; +} + +export interface BooleanTypeProps { + knob: BooleanTypeKnob; onChange: (value: BooleanTypeKnobValue) => BooleanTypeKnobValue; } diff --git a/addons/knobs/src/components/types/Button.tsx b/addons/knobs/src/components/types/Button.tsx index d5cd6d13d8cb..4dfe28b66391 100644 --- a/addons/knobs/src/components/types/Button.tsx +++ b/addons/knobs/src/components/types/Button.tsx @@ -3,13 +3,16 @@ import React, { FunctionComponent, Validator } from 'react'; import { Form } from '@storybook/components'; -interface ButtonTypeKnobProp { +export interface ButtonTypeKnob { name: string; + value: unknown; } -interface ButtonTypeProps { - knob: ButtonTypeKnobProp; - onClick: (knob: ButtonTypeKnobProp) => any; +export type ButtonTypeOnClickProp = (knob: ButtonTypeKnob) => any; + +export interface ButtonTypeProps { + knob: ButtonTypeKnob; + onClick: ButtonTypeOnClickProp; } const serialize = (): undefined => undefined; diff --git a/addons/knobs/src/components/types/Checkboxes.tsx b/addons/knobs/src/components/types/Checkboxes.tsx index 1765c014d2df..7b12cf537673 100644 --- a/addons/knobs/src/components/types/Checkboxes.tsx +++ b/addons/knobs/src/components/types/Checkboxes.tsx @@ -8,7 +8,7 @@ interface CheckboxesWrapperProps { isInline: boolean; } -interface CheckboxesTypeKnobProp { +export interface CheckboxesTypeKnob { name: string; value: CheckboxesTypeKnobValue; defaultValue: CheckboxesTypeKnobValue; @@ -18,7 +18,7 @@ interface CheckboxesTypeKnobProp { } interface CheckboxesTypeProps { - knob: CheckboxesTypeKnobProp; + knob: CheckboxesTypeKnob; isInline: boolean; onChange: (value: CheckboxesTypeKnobValue) => CheckboxesTypeKnobValue; } @@ -99,7 +99,7 @@ export default class CheckboxesType extends Component + renderCheckboxList = ({ options }: CheckboxesTypeKnob) => Object.keys(options).map(key => this.renderCheckbox(key, options[key])); renderCheckbox = (label: string, value: string) => { diff --git a/addons/knobs/src/components/types/Color.tsx b/addons/knobs/src/components/types/Color.tsx index abfa62fc772f..5c4a0afaf87a 100644 --- a/addons/knobs/src/components/types/Color.tsx +++ b/addons/knobs/src/components/types/Color.tsx @@ -9,11 +9,13 @@ import { Form } from '@storybook/components'; type ColorTypeKnobValue = string; +export interface ColorTypeKnob { + name: string; + value: ColorTypeKnobValue; +} + interface ColorTypeProps { - knob: { - name: string; - value: ColorTypeKnobValue; - }; + knob: ColorTypeKnob; onChange: (value: ColorTypeKnobValue) => ColorTypeKnobValue; } diff --git a/addons/knobs/src/components/types/Date.tsx b/addons/knobs/src/components/types/Date.tsx index e1fb6cccb665..e27b70eca10c 100644 --- a/addons/knobs/src/components/types/Date.tsx +++ b/addons/knobs/src/components/types/Date.tsx @@ -5,11 +5,13 @@ import { Form } from '@storybook/components'; type DateTypeKnobValue = number; +export interface DateTypeKnob { + name: string; + value: DateTypeKnobValue; +} + interface DateTypeProps { - knob: { - name: string; - value: DateTypeKnobValue; - }; + knob: DateTypeKnob; onChange: (value: DateTypeKnobValue) => DateTypeKnobValue; } diff --git a/addons/knobs/src/components/types/Files.tsx b/addons/knobs/src/components/types/Files.tsx index 628e7fb0f00a..34d345f97be1 100644 --- a/addons/knobs/src/components/types/Files.tsx +++ b/addons/knobs/src/components/types/Files.tsx @@ -8,12 +8,14 @@ import { Form } from '@storybook/components'; type DateTypeKnobValue = string[]; -interface FilesTypeProps { - knob: { - name: string; - accept: string; - value: DateTypeKnobValue; - }; +export interface FileTypeKnob { + name: string; + accept: string; + value: DateTypeKnobValue; +} + +export interface FilesTypeProps { + knob: FileTypeKnob; onChange: (value: DateTypeKnobValue) => DateTypeKnobValue; } diff --git a/addons/knobs/src/components/types/Number.tsx b/addons/knobs/src/components/types/Number.tsx index e27b3189ea3d..686abd9948ee 100644 --- a/addons/knobs/src/components/types/Number.tsx +++ b/addons/knobs/src/components/types/Number.tsx @@ -6,15 +6,20 @@ 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: { - name: string; - value: number; - range?: boolean; - min?: number; - max?: number; - step?: number; - }; + knob: NumberTypeKnob; onChange: (value: NumberTypeKnobValue) => NumberTypeKnobValue; } diff --git a/addons/knobs/src/components/types/Object.tsx b/addons/knobs/src/components/types/Object.tsx index 84a8f7f1c285..3000e24cabe3 100644 --- a/addons/knobs/src/components/types/Object.tsx +++ b/addons/knobs/src/components/types/Object.tsx @@ -4,11 +4,13 @@ import deepEqual from 'fast-deep-equal'; import { polyfill } from 'react-lifecycles-compat'; import { Form } from '@storybook/components'; +export interface ObjectTypeKnob { + name: string; + value: T; +} + interface ObjectTypeProps { - knob: { - name: string; - value: T; - }; + knob: ObjectTypeKnob; onChange: (value: T) => T; } diff --git a/addons/knobs/src/components/types/Options.tsx b/addons/knobs/src/components/types/Options.tsx index 23ec55ba2fce..62a972d1a415 100644 --- a/addons/knobs/src/components/types/Options.tsx +++ b/addons/knobs/src/components/types/Options.tsx @@ -21,16 +21,20 @@ export interface OptionsKnobOptions { display?: OptionsKnobOptionsDisplay; } -interface OptionsTypeProps { - knob: { - name: string; - value: T; - defaultValue: T; - options: { - [key: string]: T; - }; - optionsObj: OptionsKnobOptions; - }; +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; } diff --git a/addons/knobs/src/components/types/Radio.tsx b/addons/knobs/src/components/types/Radio.tsx index b9d9bc094c0e..9cd910f67959 100644 --- a/addons/knobs/src/components/types/Radio.tsx +++ b/addons/knobs/src/components/types/Radio.tsx @@ -4,17 +4,19 @@ import { styled } from '@storybook/theming'; type RadiosTypeKnobValue = string; -interface RadiosTypeKnobProp { +export interface RadiosTypeKnob { name: string; value: RadiosTypeKnobValue; defaultValue: RadiosTypeKnobValue; - options: { - [key: string]: RadiosTypeKnobValue; - }; + options: RadiosTypeOptionsProp; +} + +export interface RadiosTypeOptionsProp { + [key: string]: RadiosTypeKnobValue; } interface RadiosTypeProps { - knob: RadiosTypeKnobProp; + knob: RadiosTypeKnob; isInline: boolean; onChange: (value: RadiosTypeKnobValue) => RadiosTypeKnobValue; } @@ -64,7 +66,7 @@ class RadiosType extends Component { static deserialize = (value: RadiosTypeKnobValue) => value; - renderRadioButtonList({ options }: RadiosTypeKnobProp) { + renderRadioButtonList({ options }: RadiosTypeKnob) { if (Array.isArray(options)) { return options.map(val => this.renderRadioButton(val, val)); } diff --git a/addons/knobs/src/components/types/Select.tsx b/addons/knobs/src/components/types/Select.tsx index 2ecd65caac1a..e650527c0172 100644 --- a/addons/knobs/src/components/types/Select.tsx +++ b/addons/knobs/src/components/types/Select.tsx @@ -5,14 +5,18 @@ import { Form } from '@storybook/components'; type SelectTypeKnobValue = string; -interface SelectTypeProps { - knob: { - name: string; - value: SelectTypeKnobValue; - options: { - [key: string]: SelectTypeKnobValue; - }; - }; +export interface SelectTypeKnob { + name: string; + value: SelectTypeKnobValue; + options: SelectTypeOptionsProp; +} + +export interface SelectTypeOptionsProp { + [key: string]: SelectTypeKnobValue; +} + +export interface SelectTypeProps { + knob: SelectTypeKnob; onChange: (value: SelectTypeKnobValue) => SelectTypeKnobValue; } diff --git a/addons/knobs/src/components/types/Text.tsx b/addons/knobs/src/components/types/Text.tsx index c07466dc84aa..cf939ffa3f1e 100644 --- a/addons/knobs/src/components/types/Text.tsx +++ b/addons/knobs/src/components/types/Text.tsx @@ -5,11 +5,13 @@ import { Form } from '@storybook/components'; type TextTypeKnobValue = string; +export interface TextTypeKnob { + name: string; + value: TextTypeKnobValue; +} + interface TextTypeProps { - knob: { - name: string; - value: TextTypeKnobValue; - }; + knob: TextTypeKnob; onChange: (value: TextTypeKnobValue) => TextTypeKnobValue; } diff --git a/addons/knobs/src/components/types/index.ts b/addons/knobs/src/components/types/index.ts index b0c620dd94ca..c8a96d5245f0 100644 --- a/addons/knobs/src/components/types/index.ts +++ 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.ts b/addons/knobs/src/converters.ts index 7e63c8de78a3..0c07813bce0b 100644 --- a/addons/knobs/src/converters.ts +++ 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/index.ts b/addons/knobs/src/index.ts index 7035ead6c55f..4b99546a1bb8 100644 --- a/addons/knobs/src/index.ts +++ 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/registerKnobs.ts b/addons/knobs/src/registerKnobs.ts index 412783c36249..a24e3419ca3f 100644 --- a/addons/knobs/src/registerKnobs.ts +++ 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); knobOptions.callback(); forceReRender(); diff --git a/yarn.lock b/yarn.lock index 446f6c3d332a..da2de9b4caa6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3568,6 +3568,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" @@ -3739,13 +3744,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" @@ -3764,6 +3783,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" @@ -3778,6 +3806,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"