diff --git a/addons/knobs/src/components/types/Array.tsx b/addons/knobs/src/components/types/Array.tsx index 6f198878bd9d..a0a1fb81e02d 100644 --- a/addons/knobs/src/components/types/Array.tsx +++ b/addons/knobs/src/components/types/Array.tsx @@ -1,25 +1,61 @@ import PropTypes from 'prop-types'; -import React from 'react'; +import React, { Component, WeakValidationMap } from 'react'; import { Form } from '@storybook/components'; -function formatArray(value, separator) { +type ArrayTypeKnobValue = string[]; + +interface ArrayTypeProps { + knob: { + name: string; + value: ArrayTypeKnobValue; + separator: string; + }; + onChange: (value: ArrayTypeKnobValue) => ArrayTypeKnobValue; +} + +function formatArray(value: string, separator: string) { if (value === '') { return []; } return value.split(separator); } -class ArrayType extends React.Component { - shouldComponentUpdate(nextProps) { +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 => { + handleChange = (e: Event) => { const { knob, onChange } = this.props; - const { value } = e.target; + const { value } = e.target as HTMLTextAreaElement; const newVal = formatArray(value, knob.separator); onChange(newVal); @@ -40,28 +76,3 @@ class ArrayType extends React.Component { ); } } - -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/Boolean.tsx b/addons/knobs/src/components/types/Boolean.tsx index 8264f47fee28..818aa5ef1084 100644 --- a/addons/knobs/src/components/types/Boolean.tsx +++ b/addons/knobs/src/components/types/Boolean.tsx @@ -1,8 +1,19 @@ import PropTypes from 'prop-types'; -import React from 'react'; +import React, { FunctionComponent } from 'react'; import { styled } from '@storybook/theming'; +type BooleanTypeKnobValue = boolean; + +interface BooleanTypeProps { + knob: { + name: string; + value: BooleanTypeKnobValue; + separator: string; + }; + onChange: (value: BooleanTypeKnobValue) => BooleanTypeKnobValue; +} + const Input = styled.input({ display: 'table-cell', boxSizing: 'border-box', @@ -14,7 +25,13 @@ const Input = styled.input({ color: '#555', }); -const BooleanType = ({ knob, onChange }) => ( +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 }) => ( ( ); BooleanType.defaultProps = { - knob: {}, + 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 = value => (value ? String(value) : null); -BooleanType.deserialize = value => value === 'true'; +BooleanType.serialize = serialize; +BooleanType.deserialize = deserialize; export default BooleanType; diff --git a/addons/knobs/src/components/types/Button.tsx b/addons/knobs/src/components/types/Button.tsx index 39b0a07f5780..d5cd6d13d8cb 100644 --- a/addons/knobs/src/components/types/Button.tsx +++ b/addons/knobs/src/components/types/Button.tsx @@ -1,22 +1,42 @@ import PropTypes from 'prop-types'; -import React from 'react'; +import React, { FunctionComponent, Validator } from 'react'; import { Form } from '@storybook/components'; -const ButtonType = ({ knob, onClick }) => ( +interface ButtonTypeKnobProp { + name: string; +} + +interface ButtonTypeProps { + knob: ButtonTypeKnobProp; + onClick: (knob: ButtonTypeKnobProp) => any; +} + +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, + }).isRequired as Validator, onClick: PropTypes.func.isRequired, }; -ButtonType.serialize = () => undefined; -ButtonType.deserialize = () => undefined; +ButtonType.serialize = serialize; +ButtonType.deserialize = deserialize; export default ButtonType; diff --git a/addons/knobs/src/components/types/Checkboxes.tsx b/addons/knobs/src/components/types/Checkboxes.tsx index a15cf1d64126..1765c014d2df 100644 --- a/addons/knobs/src/components/types/Checkboxes.tsx +++ b/addons/knobs/src/components/types/Checkboxes.tsx @@ -1,8 +1,33 @@ -import React, { Component } from 'react'; +import React, { Component, ChangeEvent, WeakValidationMap } from 'react'; import PropTypes from 'prop-types'; import { styled } from '@storybook/theming'; -const CheckboxesWrapper = styled.div(({ isInline }) => +type CheckboxesTypeKnobValue = string[]; + +interface CheckboxesWrapperProps { + isInline: boolean; +} + +interface CheckboxesTypeKnobProp { + name: string; + value: CheckboxesTypeKnobValue; + defaultValue: CheckboxesTypeKnobValue; + options: { + [key: string]: string; + }; +} + +interface CheckboxesTypeProps { + knob: CheckboxesTypeKnobProp; + isInline: boolean; + onChange: (value: CheckboxesTypeKnobValue) => CheckboxesTypeKnobValue; +} + +interface CheckboxesTypeState { + values: CheckboxesTypeKnobValue; +} + +const CheckboxesWrapper = styled.div(({ isInline }: CheckboxesWrapperProps) => isInline ? { display: 'flex', @@ -27,8 +52,29 @@ const CheckboxLabel = styled.label({ display: 'inline-block', }); -class CheckboxesType extends Component { - constructor(props) { +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; @@ -37,9 +83,9 @@ class CheckboxesType extends Component { }; } - handleChange = e => { + handleChange = (e: ChangeEvent) => { const { onChange } = this.props; - const currentValue = e.target.value; + const currentValue = (e.target as HTMLInputElement).value; const { values } = this.state; if (values.includes(currentValue)) { @@ -53,10 +99,10 @@ class CheckboxesType extends Component { onChange(values); }; - renderCheckboxList = ({ options }) => + renderCheckboxList = ({ options }: CheckboxesTypeKnobProp) => Object.keys(options).map(key => this.renderCheckbox(key, options[key])); - renderCheckbox = (label, value) => { + renderCheckbox = (label: string, value: string) => { const { knob } = this.props; const { name } = knob; const id = `${name}-${value}`; @@ -87,24 +133,3 @@ class CheckboxesType extends Component { ); } } - -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/Color.tsx b/addons/knobs/src/components/types/Color.tsx index 1b440d615616..43c3c2a20f5e 100644 --- a/addons/knobs/src/components/types/Color.tsx +++ b/addons/knobs/src/components/types/Color.tsx @@ -1,12 +1,45 @@ +// @ts-ignore import { document } from 'global'; import PropTypes from 'prop-types'; -import React from 'react'; +import React, { Component, WeakValidationMap } from 'react'; +// @ts-ignore import { SketchPicker } from 'react-color'; import { styled } from '@storybook/theming'; - import { Form } from '@storybook/components'; +type ColorTypeKnobValue = string; + +interface ColorTypeProps { + knob: { + name: string; + value: ColorTypeKnobValue; + }; + onChange: (value: ColorTypeKnobValue) => ColorTypeKnobValue; +} + +interface ColorTypeState { + displayColorPicker: boolean; +} + +interface ColorButtonProps { + name: string; + type: string; + size: string; + active: boolean; + onClick: () => any; +} + +// TODO: These types should come from @types/react-color once installed +interface ColorResult { + rgb: { + a?: number; + b: number; + g: number; + r: number; + }; +} + const { Button } = Form; const Swatch = styled.div(({ theme }) => ({ @@ -20,25 +53,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 +104,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 +123,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 +158,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.tsx b/addons/knobs/src/components/types/Date.tsx index f645fcca9e68..e1fb6cccb665 100644 --- a/addons/knobs/src/components/types/Date.tsx +++ b/addons/knobs/src/components/types/Date.tsx @@ -1,8 +1,22 @@ -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; + +interface DateTypeProps { + knob: { + name: string; + value: DateTypeKnobValue; + }; + onChange: (value: DateTypeKnobValue) => DateTypeKnobValue; +} + +interface DateTypeState { + valid: boolean | undefined; +} + const FlexSpaced = styled.div({ flex: 1, display: 'flex', @@ -15,29 +29,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 +88,7 @@ class DateType extends Component { } } - onDateChange = e => { + onDateChange = (e: ChangeEvent) => { const { knob, onChange } = this.props; const { state } = this; @@ -70,7 +109,7 @@ class DateType extends Component { } }; - onTimeChange = e => { + onTimeChange = (e: ChangeEvent) => { const { knob, onChange } = this.props; const { state } = this; @@ -100,7 +139,7 @@ class DateType extends Component { { + ref={(el: HTMLInputElement) => { this.dateInput = el; }} id={`${name}date`} @@ -111,7 +150,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 +160,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.tsx b/addons/knobs/src/components/types/Files.tsx index 797498d2b935..628e7fb0f00a 100644 --- a/addons/knobs/src/components/types/Files.tsx +++ b/addons/knobs/src/components/types/Files.tsx @@ -1,46 +1,67 @@ +// @ts-ignore import { FileReader } from 'global'; import PropTypes from 'prop-types'; -import React from 'react'; +import React, { ChangeEvent, FunctionComponent } from 'react'; import { styled } from '@storybook/theming'; import { Form } from '@storybook/components'; +type DateTypeKnobValue = string[]; + +interface FilesTypeProps { + knob: { + name: string; + accept: string; + value: DateTypeKnobValue; + }; + onChange: (value: DateTypeKnobValue) => DateTypeKnobValue; +} + const FileInput = styled(Form.Input)({ paddingTop: 4, }); -function fileReaderPromise(file) { - return new Promise(resolve => { +function fileReaderPromise(file: File) { + return new Promise(resolve => { const fileReader = new FileReader(); - fileReader.onload = e => resolve(e.currentTarget.result); + fileReader.onload = (e: Event) => resolve((e.currentTarget as FileReader).result); fileReader.readAsDataURL(file); }); } -const FilesType = ({ knob, onChange }) => ( +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)} + onChange={(e: ChangeEvent) => + Promise.all(Array.from(e.target.files).map(fileReaderPromise)).then(onChange) + } accept={knob.accept} size="flex" /> ); FilesType.defaultProps = { - knob: {}, + 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 = () => undefined; -FilesType.deserialize = () => undefined; +FilesType.serialize = serialize; +FilesType.deserialize = deserialize; export default FilesType; diff --git a/addons/knobs/src/components/types/Number.tsx b/addons/knobs/src/components/types/Number.tsx index 9a6b677cb8f5..e27b3189ea3d 100644 --- a/addons/knobs/src/components/types/Number.tsx +++ b/addons/knobs/src/components/types/Number.tsx @@ -1,45 +1,78 @@ import PropTypes from 'prop-types'; -import React from 'react'; +import React, { Component, ChangeEvent } 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, -}); +type NumberTypeKnobValue = number; + +interface NumberTypeProps { + knob: { + name: string; + value: number; + range?: boolean; + min?: number; + max?: number; + step?: number; + }; + 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%', }); -class NumberType extends React.Component { - shouldComponentUpdate(nextProps) { +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 => { + handleChange = (event: ChangeEvent) => { const { onChange } = this.props; const { value } = event.target; @@ -83,20 +116,3 @@ class NumberType extends React.Component { ); } } - -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/Object.tsx b/addons/knobs/src/components/types/Object.tsx index b68e55e24fa5..c3a3c245c86b 100644 --- a/addons/knobs/src/components/types/Object.tsx +++ b/addons/knobs/src/components/types/Object.tsx @@ -1,17 +1,41 @@ -import React, { Component } from 'react'; +import React, { Component, ChangeEvent } from 'react'; import PropTypes from 'prop-types'; import deepEqual from 'fast-deep-equal'; +// @ts-ignore import { polyfill } from 'react-lifecycles-compat'; import { Form } from '@storybook/components'; -class ObjectType extends Component { - state = { - value: {}, - failed: false, - json: '', +interface ObjectTypeProps { + knob: { + name: string; + value: T; }; + 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 serialize: { (object: T): string } = object => JSON.stringify(object); - static getDerivedStateFromProps(props, state) { + 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 +50,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 +95,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.tsx b/addons/knobs/src/components/types/Options.tsx index 8580f72d7d44..0ce43e5b17cd 100644 --- a/addons/knobs/src/components/types/Options.tsx +++ b/addons/knobs/src/components/types/Options.tsx @@ -1,5 +1,6 @@ -import React from 'react'; +import React, { FunctionComponent } from 'react'; import PropTypes from 'prop-types'; +// @ts-ignore import ReactSelect from 'react-select'; import { styled } from '@storybook/theming'; @@ -8,13 +9,67 @@ import CheckboxesType from './Checkboxes'; // TODO: Apply the Storybook theme to react-select -const OptionsSelect = styled(ReactSelect)({ +export type OptionsKnobOptionsDisplay = + | 'radio' + | 'inline-radio' + | 'check' + | 'inline-check' + | 'select' + | 'multi-select'; + +export interface OptionsKnobOptions { + display?: OptionsKnobOptionsDisplay; +} + +interface OptionsTypeProps { + knob: { + name: string; + value: T; + defaultValue: T; + options: { + [key: string]: T; + }; + optionsObj: OptionsKnobOptions; + }; + display: OptionsKnobOptionsDisplay; + onChange: (value: T) => T; +} + +const OptionsSelect: React.ComponentType = styled(ReactSelect)({ width: '100%', maxWidth: '300px', color: 'black', }); -const OptionsType = props => { +// TODO: These types should come from @types/react-select once installed. +type ReactSelectValueType = + | OptionType + | OptionsType + | null + | undefined; + +type ReactSelectOnChangeFn = ( + value: ReactSelectValueType +) => void; + +interface ReactSelectProps { + value: OptionsSelectValueItem | OptionsSelectValueItem[]; + options: any; + isMulti: boolean; + onChange: ReactSelectOnChangeFn; +} +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; @@ -29,19 +84,19 @@ const OptionsType = props => { } if (display === 'select' || display === 'multi-select') { - const options = Object.keys(knob.options).map(key => ({ + 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 = options[optionsIndex]; - let handleChange = e => onChange(e.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 => onChange(values.map(item => item.value)); + handleChange = (values: OptionsSelectValueItem[]) => onChange(values.map(item => item.value)); } return ( @@ -53,33 +108,35 @@ const OptionsType = props => { /> ); } + return null; }; OptionsType.defaultProps = { - knob: {}, + 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, - }), - display: PropTypes.oneOf([ - 'check', - 'inline-check', + }) as any, + display: PropTypes.oneOf([ 'radio', 'inline-radio', + 'check', + 'inline-check', 'select', 'multi-select', ]), onChange: PropTypes.func, }; -OptionsType.serialize = value => value; -OptionsType.deserialize = value => value; +OptionsType.serialize = serialize; +OptionsType.deserialize = deserialize; export default OptionsType; diff --git a/addons/knobs/src/components/types/Radio.tsx b/addons/knobs/src/components/types/Radio.tsx index f4163967e2ea..b9d9bc094c0e 100644 --- a/addons/knobs/src/components/types/Radio.tsx +++ b/addons/knobs/src/components/types/Radio.tsx @@ -1,8 +1,29 @@ -import React, { Component } from 'react'; +import React, { Component, WeakValidationMap } from 'react'; import PropTypes from 'prop-types'; import { styled } from '@storybook/theming'; -const RadiosWrapper = styled.div(({ isInline }) => +type RadiosTypeKnobValue = string; + +interface RadiosTypeKnobProp { + name: string; + value: RadiosTypeKnobValue; + defaultValue: RadiosTypeKnobValue; + options: { + [key: string]: RadiosTypeKnobValue; + }; +} + +interface RadiosTypeProps { + knob: RadiosTypeKnobProp; + isInline: boolean; + onChange: (value: RadiosTypeKnobValue) => RadiosTypeKnobValue; +} + +interface RadiosWrapperProps { + isInline: boolean; +} + +const RadiosWrapper = styled.div(({ isInline }: RadiosWrapperProps) => isInline ? { display: 'flex', @@ -21,15 +42,36 @@ const RadioLabel = styled.label({ display: 'inline-block', }); -class RadiosType extends Component { - renderRadioButtonList({ options }) { +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 }: RadiosTypeKnobProp) { 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) { + renderRadioButton(label: string, value: RadiosTypeKnobValue) { const opts = { label, value }; const { onChange, knob } = this.props; const { name } = knob; @@ -57,23 +99,4 @@ class RadiosType extends Component { } } -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/Select.tsx b/addons/knobs/src/components/types/Select.tsx index ff3809505ff5..2ecd65caac1a 100644 --- a/addons/knobs/src/components/types/Select.tsx +++ b/addons/knobs/src/components/types/Select.tsx @@ -1,9 +1,28 @@ -import React from 'react'; +import React, { FunctionComponent, ChangeEvent } from 'react'; import PropTypes from 'prop-types'; import { Form } from '@storybook/components'; -const SelectType = ({ knob, onChange }) => { +type SelectTypeKnobValue = string; + +interface SelectTypeProps { + knob: { + name: string; + value: SelectTypeKnobValue; + options: { + [key: string]: SelectTypeKnobValue; + }; + }; + 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 }), {}) @@ -15,7 +34,7 @@ const SelectType = ({ knob, onChange }) => { { + onChange={(e: ChangeEvent) => { onChange(entries[e.target.value]); }} size="flex" @@ -30,20 +49,21 @@ const SelectType = ({ knob, onChange }) => { }; SelectType.defaultProps = { - knob: {}, + 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 = value => value; -SelectType.deserialize = value => value; +SelectType.serialize = serialize; +SelectType.deserialize = deserialize; export default SelectType; diff --git a/addons/knobs/src/components/types/Text.tsx b/addons/knobs/src/components/types/Text.tsx index 2f95e8f5b0f0..c07466dc84aa 100644 --- a/addons/knobs/src/components/types/Text.tsx +++ b/addons/knobs/src/components/types/Text.tsx @@ -1,16 +1,44 @@ import PropTypes from 'prop-types'; -import React from 'react'; +import React, { Component, ChangeEvent, WeakValidationMap } from 'react'; import { Form } from '@storybook/components'; -class TextType extends React.Component { - shouldComponentUpdate(nextProps) { +type TextTypeKnobValue = string; + +interface TextTypeProps { + knob: { + name: string; + value: TextTypeKnobValue; + }; + 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 => { + handleChange = (event: ChangeEvent) => { const { onChange } = this.props; const { value } = event.target; @@ -31,21 +59,3 @@ class TextType extends React.Component { ); } } - -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;