diff --git a/addons/knobs/src/KnobManager.ts b/addons/knobs/src/KnobManager.ts index a79c855ae5fb..fe875efd61ea 100644 --- a/addons/knobs/src/KnobManager.ts +++ b/addons/knobs/src/KnobManager.ts @@ -6,7 +6,8 @@ import { getQueryParams } from '@storybook/client-api'; // eslint-disable-next-line import/no-extraneous-dependencies import { Channel } from '@storybook/channels'; -import KnobStore, { Knob } from './KnobStore'; +import KnobStore, { KnobStoreKnob } from './KnobStore'; +import { Knob, KnobType } from './type-defs'; import { SET } from './shared'; import { deserializers } from './converters'; @@ -71,7 +72,7 @@ export default class KnobManager { return this.options.escapeHTML ? escapeStrings(value) : value; } - knob(name: string, options: Knob) { + knob(name: string, options: Knob): Knob['value'] { this._mayCallChannel(); const knobName = options.groupId ? `${name}_${options.groupId}` : name; @@ -93,7 +94,7 @@ export default class KnobManager { return this.getKnobValue(existingKnob); } - const knobInfo: Knob & { name: string; label: string; defaultValue?: any } = { + const knobInfo: Knob & { name: string; label: string; defaultValue?: any } = { ...options, name: knobName, label: name, @@ -110,7 +111,7 @@ export default class KnobManager { knobInfo.defaultValue = options.value; } - knobStore.set(knobName, knobInfo); + knobStore.set(knobName, knobInfo as KnobStoreKnob); return this.getKnobValue(knobStore.get(knobName)); } diff --git a/addons/knobs/src/KnobStore.ts b/addons/knobs/src/KnobStore.ts index 5ee42ffd8e8a..31b4207e8edb 100644 --- a/addons/knobs/src/KnobStore.ts +++ b/addons/knobs/src/KnobStore.ts @@ -1,36 +1,7 @@ -import Types, { - TextTypeKnob, - NumberTypeKnob, - ColorTypeKnob, - BooleanTypeKnob, - ObjectTypeKnob, - SelectTypeKnob, - RadiosTypeKnob, - ArrayTypeKnob, - DateTypeKnob, - ButtonTypeOnClickProp, - FileTypeKnob, - OptionsTypeKnob, -} from './components/types'; +import { Knob } from './type-defs'; 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; diff --git a/addons/knobs/src/components/Panel.tsx b/addons/knobs/src/components/Panel.tsx index de5a3ceababc..4154e6359992 100644 --- a/addons/knobs/src/components/Panel.tsx +++ b/addons/knobs/src/components/Panel.tsx @@ -1,4 +1,4 @@ -import React, { PureComponent, Fragment, ComponentType } from 'react'; +import React, { PureComponent, Fragment } from 'react'; import PropTypes from 'prop-types'; import qs from 'qs'; import { document } from 'global'; @@ -16,7 +16,7 @@ import { } from '@storybook/components'; import { RESET, SET, CHANGE, SET_OPTIONS, CLICK } from '../shared'; -import Types from './types'; +import { getKnobControl } from './types'; import PropForm from './PropForm'; import { KnobStoreKnob } from '../KnobStore'; @@ -58,11 +58,6 @@ 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, @@ -133,9 +128,9 @@ export default class KnobPanel extends PureComponent { // If the knob value present in url if (urlValue !== undefined) { - const value = (Types[knob.type] as KnobControlType).deserialize(urlValue); + const value = getKnobControl(knob.type).deserialize(urlValue); knob.value = value; - queryParams[`knob-${name}`] = (Types[knob.type] as KnobControlType).serialize(value); + queryParams[`knob-${name}`] = getKnobControl(knob.type).serialize(value); api.emit(CHANGE, knob); } @@ -161,7 +156,7 @@ export default class KnobPanel extends PureComponent { const { knobs } = this.state; Object.entries(knobs).forEach(([name, knob]) => { - query[`knob-${name}`] = (Types[knob.type] as KnobControlType).serialize(knob.value); + query[`knob-${name}`] = getKnobControl(knob.type).serialize(knob.value); }); copy(`${location.origin + location.pathname}?${qs.stringify(query, { encode: false })}`); @@ -193,7 +188,7 @@ export default class KnobPanel extends PureComponent { Object.keys(newKnobs).forEach(n => { const knob = newKnobs[n]; - queryParams[`knob-${n}`] = (Types[knob.type] as KnobControlType).serialize(knob.value); + queryParams[`knob-${n}`] = getKnobControl(knob.type).serialize(knob.value); }); api.setQueryParams(queryParams); diff --git a/addons/knobs/src/components/PropForm.tsx b/addons/knobs/src/components/PropForm.tsx index 5b4c381151f9..3a345a67f0bf 100644 --- a/addons/knobs/src/components/PropForm.tsx +++ b/addons/knobs/src/components/PropForm.tsx @@ -2,7 +2,7 @@ import React, { Component, WeakValidationMap, ComponentType, Requireable } from import PropTypes from 'prop-types'; import { Form } from '@storybook/components'; -import TypeMap from './types'; +import { getKnobControl } from './types'; import { KnobStoreKnob } from '../KnobStore'; interface PropFormProps { @@ -48,7 +48,7 @@ export default class PropForm extends Component {
{knobs.map(knob => { const changeHandler = this.makeChangeHandler(knob.name, knob.type); - const InputType: ComponentType = TypeMap[knob.type] || InvalidType; + const InputType: ComponentType = getKnobControl(knob.type) || InvalidType; return ( diff --git a/addons/knobs/src/components/types/index.ts b/addons/knobs/src/components/types/index.ts index a4a727445b3d..1a25315f8d0b 100644 --- a/addons/knobs/src/components/types/index.ts +++ b/addons/knobs/src/components/types/index.ts @@ -1,3 +1,5 @@ +import { ComponentType } from 'react'; + import TextType from './Text'; import NumberType from './Number'; import ColorType from './Color'; @@ -11,7 +13,7 @@ import ButtonType from './Button'; import FilesType from './Files'; import OptionsType from './Options'; -export default { +const KnobControls = { text: TextType, number: NumberType, color: ColorType, @@ -25,6 +27,17 @@ export default { files: FilesType, options: OptionsType, }; +export default KnobControls; + +export type KnobType = keyof typeof KnobControls; + +export type KnobControlType = ComponentType & { + serialize: (v: any) => any; + deserialize: (v: any) => any; +}; + +// Note: this is a utility function that helps in resolving types more orderly +export const getKnobControl = (type: KnobType): KnobControlType => KnobControls[type]; export { TextTypeKnob } from './Text'; export { NumberTypeKnob, NumberTypeKnobOptions } from './Number'; diff --git a/addons/knobs/src/index.ts b/addons/knobs/src/index.ts index 0924ad08a659..c7a060c5785c 100644 --- a/addons/knobs/src/index.ts +++ b/addons/knobs/src/index.ts @@ -2,6 +2,7 @@ import addons, { makeDecorator } from '@storybook/addons'; import { SET_OPTIONS } from './shared'; import { manager, registerKnobs } from './registerKnobs'; +import { Knob, KnobType } from './type-defs'; import { NumberTypeKnobOptions, ButtonTypeOnClickProp, @@ -12,8 +13,8 @@ import { OptionsKnobOptions, } from './components/types'; -export function knob(name: string, optionsParam: any) { - return manager.knob(name, optionsParam); +export function knob(name: string, options: Knob) { + return manager.knob(name, options); } export function text(name: string, value: string, groupId?: string) { @@ -57,7 +58,7 @@ export function color(name: string, value: string, groupId?: string) { return manager.knob(name, { type: 'color', value, groupId }); } -export function object(name: string, value: T, groupId?: string) { +export function object(name: string, value: T, groupId?: string): T { return manager.knob(name, { type: 'object', value, groupId }); } @@ -99,10 +100,10 @@ export function files(name: string, accept: string, value: string[] = [], groupI export function optionsKnob( name: string, valuesObj: OptionsTypeOptionsProp, - value: string, + value: T, optionsObj: OptionsKnobOptions, groupId?: string -) { +): T { return manager.knob(name, { type: 'options', options: valuesObj, value, optionsObj, groupId }); } diff --git a/addons/knobs/src/type-defs.ts b/addons/knobs/src/type-defs.ts new file mode 100644 index 000000000000..51c76c6c2b39 --- /dev/null +++ b/addons/knobs/src/type-defs.ts @@ -0,0 +1,45 @@ +import { + TextTypeKnob, + NumberTypeKnob, + ColorTypeKnob, + BooleanTypeKnob, + ObjectTypeKnob, + SelectTypeKnob, + RadiosTypeKnob, + ArrayTypeKnob, + DateTypeKnob, + ButtonTypeOnClickProp, + FileTypeKnob, + OptionsTypeKnob, + KnobType, +} from './components/types'; + +type KnobPlus = K & { type: T; groupId?: string }; + +export type Knob = T extends 'text' + ? KnobPlus> + : T extends 'boolean' + ? KnobPlus> + : T extends 'number' + ? KnobPlus> + : T extends 'color' + ? KnobPlus> + : T extends 'object' + ? KnobPlus, 'value'>> + : T extends 'select' + ? KnobPlus & { selectV2: true }> + : T extends 'radios' + ? KnobPlus> + : T extends 'array' + ? KnobPlus> + : T extends 'date' + ? KnobPlus> + : T extends 'files' + ? KnobPlus> + : T extends 'button' + ? KnobPlus + : T extends 'options' + ? KnobPlus, 'options' | 'value' | 'optionsObj'>> + : never; + +export { KnobType };