Controls: Remove react-select and fix initialization logic

This commit is contained in:
Michael Shilman 2020-06-03 08:53:57 +08:00
parent 29a13f6657
commit e11b64df92
8 changed files with 130 additions and 81 deletions

View File

@ -11,20 +11,14 @@ import { MemoButton } from '../../components/MemoButton';
parameters={{ controls: { expanded: false } }} parameters={{ controls: { expanded: false } }}
/> />
export const FooBar = ({ foo, bar, baz } = {}) => ( export const ArgsDisplay = (args = {}) => (
<table> <table>
{Object.entries(args).map(([key, val]) => (
<tr> <tr>
<td>Foo</td> <td>{key}</td>
<td>{foo && foo.toString()}</td> <td>{val && val.toString()}</td>
</tr>
<tr>
<td>Bar</td>
<td>{bar}</td>
</tr>
<tr>
<td>Baz</td>
<td>{baz && baz.toString()}</td>
</tr> </tr>
))}
</table> </table>
); );
@ -34,21 +28,45 @@ export const FooBar = ({ foo, bar, baz } = {}) => (
<Story <Story
name="ArgTypes" name="ArgTypes"
args={{ args={{
foo: false, boolArg: true,
bar: '', stringArg: 'overwritten',
baz: ['a', 'b'], arrayArg: ['a', 'b'],
}} }}
argTypes={{ argTypes={{
foo: { name: 'foo', type: { name: 'boolean' }, description: 'foo description' }, boolArg: { name: 'boolArg', type: { name: 'boolean' }, description: 'bool description' },
bar: { name: 'bar', type: { name: 'string' }, description: 'bar description' }, stringArg: {
baz: { name: 'stringArg',
name: 'baz', type: { name: 'string' },
description: 'bar description',
defaultValue: 'bar default',
table: {
defaultValue: {
summary: 'bar def',
detail: 'some long bar default',
},
},
},
arrayArg: {
name: 'arrayArg',
type: { name: 'array', value: { name: 'string' } }, type: { name: 'array', value: { name: 'string' } },
description: 'baz description', description: 'baz description',
}, },
selectArg: {
name: 'selectArg',
type: { name: 'enum' },
defaultValue: 2,
control: {
type: 'select',
options: {
a: 1,
b: 2,
c: 3,
},
},
},
}} }}
> >
{(args) => <FooBar {...args} />} {(args) => <ArgsDisplay {...args} />}
</Story> </Story>
</Preview> </Preview>
@ -68,7 +86,7 @@ export const FooBar = ({ foo, bar, baz } = {}) => (
bar: '', bar: '',
}} }}
> >
{(args) => <FooBar {...args} />} {(args) => <ArgsDisplay {...args} />}
</Story> </Story>
</Preview> </Preview>

View File

@ -50,7 +50,6 @@
"react-dom": "^16.8.3", "react-dom": "^16.8.3",
"react-helmet-async": "^1.0.2", "react-helmet-async": "^1.0.2",
"react-popper-tooltip": "^2.11.0", "react-popper-tooltip": "^2.11.0",
"react-select": "^3.0.8",
"react-syntax-highlighter": "^12.2.1", "react-syntax-highlighter": "^12.2.1",
"react-textarea-autosize": "^7.1.0", "react-textarea-autosize": "^7.1.0",
"ts-dedent": "^1.1.1" "ts-dedent": "^1.1.1"

View File

@ -1,6 +1,7 @@
import React, { FC, ChangeEvent, useState } from 'react'; import React, { FC, ChangeEvent, useState } from 'react';
import { styled } from '@storybook/theming'; import { styled } from '@storybook/theming';
import { ControlProps, OptionsMultiSelection, NormalizedOptionsConfig } from '../types'; import { ControlProps, OptionsMultiSelection, NormalizedOptionsConfig } from '../types';
import { selectedKeys, selectedValues } from './helpers';
const CheckboxesWrapper = styled.div<{ isInline: boolean }>(({ isInline }) => const CheckboxesWrapper = styled.div<{ isInline: boolean }>(({ isInline }) =>
isInline isInline
@ -36,18 +37,19 @@ export const CheckboxControl: FC<CheckboxProps> = ({
onChange, onChange,
isInline, isInline,
}) => { }) => {
const [selected, setSelected] = useState(value || []); const initial = selectedKeys(value, options);
const [selected, setSelected] = useState(initial || []);
const handleChange = (e: ChangeEvent<HTMLInputElement>) => { const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
const option = (e.target as HTMLInputElement).value; const option = (e.target as HTMLInputElement).value;
const newVal = [...selected]; const updated = [...selected];
if (newVal.includes(option)) { if (updated.includes(option)) {
newVal.splice(newVal.indexOf(option), 1); updated.splice(updated.indexOf(option), 1);
} else { } else {
newVal.push(option); updated.push(option);
} }
onChange(name, newVal); onChange(name, selectedValues(updated, options));
setSelected(newVal); setSelected(updated);
}; };
return ( return (
@ -55,17 +57,15 @@ export const CheckboxControl: FC<CheckboxProps> = ({
<CheckboxesWrapper isInline={isInline}> <CheckboxesWrapper isInline={isInline}>
{Object.keys(options).map((key: string) => { {Object.keys(options).map((key: string) => {
const id = `${name}-${key}`; const id = `${name}-${key}`;
const optionValue = options[key];
return ( return (
<div key={id}> <div key={id}>
<input <input
type="checkbox" type="checkbox"
id={id} id={id}
name={name} name={name}
value={optionValue} value={key}
onChange={handleChange} onChange={handleChange}
checked={selected.includes(optionValue)} checked={selected.includes(key)}
/> />
<CheckboxLabel htmlFor={id}>{key}</CheckboxLabel> <CheckboxLabel htmlFor={id}>{key}</CheckboxLabel>
</div> </div>

View File

@ -12,10 +12,10 @@ const objectOptions = {
B: { id: 'Bat' }, B: { id: 'Bat' },
C: { id: 'Cat' }, C: { id: 'Cat' },
}; };
const emptyOptions = null;
const optionsHelper = (options, type) => { const optionsHelper = (options, type, isMulti) => {
const [value, setValue] = useState([]); const initial = Array.isArray(options) ? options[1] : options.B;
const [value, setValue] = useState(isMulti ? [initial] : initial);
return ( return (
<> <>
<OptionsControl <OptionsControl
@ -23,7 +23,7 @@ const optionsHelper = (options, type) => {
options={options} options={options}
value={value} value={value}
type={type} type={type}
onChange={(name, newVal) => setValue(newVal)} onChange={(_name, newVal) => setValue(newVal)}
/> />
{value && Array.isArray(value) ? ( {value && Array.isArray(value) ? (
// eslint-disable-next-line react/no-array-index-key // eslint-disable-next-line react/no-array-index-key
@ -36,19 +36,19 @@ const optionsHelper = (options, type) => {
}; };
// Check // Check
export const CheckArray = () => optionsHelper(arrayOptions, 'check'); export const CheckArray = () => optionsHelper(arrayOptions, 'check', true);
export const InlineCheckArray = () => optionsHelper(arrayOptions, 'inline-check'); export const InlineCheckArray = () => optionsHelper(arrayOptions, 'inline-check', true);
export const CheckObject = () => optionsHelper(objectOptions, 'check'); export const CheckObject = () => optionsHelper(objectOptions, 'check', true);
export const InlineCheckObject = () => optionsHelper(objectOptions, 'inline-check'); export const InlineCheckObject = () => optionsHelper(objectOptions, 'inline-check', true);
// Radio // Radio
export const ArrayRadio = () => optionsHelper(arrayOptions, 'radio'); export const ArrayRadio = () => optionsHelper(arrayOptions, 'radio', false);
export const ArrayInlineRadio = () => optionsHelper(arrayOptions, 'inline-radio'); export const ArrayInlineRadio = () => optionsHelper(arrayOptions, 'inline-radio', false);
export const ObjectRadio = () => optionsHelper(objectOptions, 'radio'); export const ObjectRadio = () => optionsHelper(objectOptions, 'radio', false);
export const ObjectInlineRadio = () => optionsHelper(objectOptions, 'inline-radio'); export const ObjectInlineRadio = () => optionsHelper(objectOptions, 'inline-radio', false);
// Select // Select
export const ArraySelect = () => optionsHelper(arrayOptions, 'select'); export const ArraySelect = () => optionsHelper(arrayOptions, 'select', false);
export const ArrayMultiSelect = () => optionsHelper(arrayOptions, 'multi-select'); export const ArrayMultiSelect = () => optionsHelper(arrayOptions, 'multi-select', true);
export const ObjectSelect = () => optionsHelper(objectOptions, 'select'); export const ObjectSelect = () => optionsHelper(objectOptions, 'select', false);
export const ObjectMultiSelect = () => optionsHelper(objectOptions, 'multi-select'); export const ObjectMultiSelect = () => optionsHelper(objectOptions, 'multi-select', true);

View File

@ -5,10 +5,18 @@ import { RadioControl } from './Radio';
import { SelectControl } from './Select'; import { SelectControl } from './Select';
import { ControlProps, OptionsSelection, OptionsConfig, Options } from '../types'; import { ControlProps, OptionsSelection, OptionsConfig, Options } from '../types';
/**
* Options can accept `options` in two formats:
* - array: ['a', 'b', 'c'] OR
* - object: { a: 1, b: 2, c: 3 }
*
* We always normalize to the more generalized object format and ONLY handle
* the object format in the underlying control implementations.
*/
const normalizeOptions = (options: Options) => { const normalizeOptions = (options: Options) => {
if (Array.isArray(options)) { if (Array.isArray(options)) {
return options.reduce((acc, item) => { return options.reduce((acc, item) => {
acc[item] = item; acc[item.toString()] = item;
return acc; return acc;
}, {}); }, {});
} }

View File

@ -1,6 +1,7 @@
import React, { FC, Validator } from 'react'; import React, { FC, Validator } from 'react';
import { styled } from '@storybook/theming'; import { styled } from '@storybook/theming';
import { ControlProps, OptionsSingleSelection, NormalizedOptionsConfig } from '../types'; import { ControlProps, OptionsSingleSelection, NormalizedOptionsConfig } from '../types';
import { selectedKey, selectedKeys } from './helpers';
const RadiosWrapper = styled.div<{ isInline: boolean }>(({ isInline }) => const RadiosWrapper = styled.div<{ isInline: boolean }>(({ isInline }) =>
isInline isInline
@ -24,20 +25,20 @@ const RadioLabel = styled.label({
type RadioConfig = NormalizedOptionsConfig & { isInline: boolean }; type RadioConfig = NormalizedOptionsConfig & { isInline: boolean };
type RadioProps = ControlProps<OptionsSingleSelection> & RadioConfig; type RadioProps = ControlProps<OptionsSingleSelection> & RadioConfig;
export const RadioControl: FC<RadioProps> = ({ name, options, value, onChange, isInline }) => { export const RadioControl: FC<RadioProps> = ({ name, options, value, onChange, isInline }) => {
const selection = selectedKey(value, options);
return ( return (
<RadiosWrapper isInline={isInline}> <RadiosWrapper isInline={isInline}>
{Object.keys(options).map((key) => { {Object.keys(options).map((key) => {
const id = `${name}-${key}`; const id = `${name}-${key}`;
const optionValue = options[key];
return ( return (
<div key={id}> <div key={id}>
<input <input
type="radio" type="radio"
id={id} id={id}
name={name} name={name}
value={optionValue || undefined} value={key}
onChange={(e) => onChange(name, e.target.value)} onChange={(e) => onChange(name, options[e.currentTarget.value])}
checked={optionValue === value} checked={key === selection}
/> />
<RadioLabel htmlFor={id}>{key}</RadioLabel> <RadioLabel htmlFor={id}>{key}</RadioLabel>
</div> </div>

View File

@ -1,41 +1,50 @@
import React, { FC } from 'react'; import React, { FC, ChangeEvent } from 'react';
import ReactSelect from 'react-select';
import { styled } from '@storybook/theming'; import { styled } from '@storybook/theming';
import { ControlProps, OptionsSelection, NormalizedOptionsConfig } from '../types'; import { ControlProps, OptionsSelection, NormalizedOptionsConfig } from '../types';
import { selectedKey, selectedKeys, selectedValues } from './helpers';
// TODO: Apply the Storybook theme to react-select const OptionsSelect = styled.select({
const OptionsSelect = styled(ReactSelect)({
width: '100%', width: '100%',
maxWidth: '300px', maxWidth: '300px',
color: 'black', color: 'black',
}); });
interface OptionsItem {
value: any;
label: string;
}
type ReactSelectOnChangeFn = { (v: OptionsItem): void } | { (v: OptionsItem[]): void };
type SelectConfig = NormalizedOptionsConfig & { isMulti: boolean }; type SelectConfig = NormalizedOptionsConfig & { isMulti: boolean };
type SelectProps = ControlProps<OptionsSelection> & SelectConfig; type SelectProps = ControlProps<OptionsSelection> & SelectConfig;
export const SelectControl: FC<SelectProps> = ({ name, value, options, onChange, isMulti }) => {
// const optionsIndex = options.findIndex(i => i.value === value);
// let defaultValue: typeof options | typeof options[0] = options[optionsIndex];
const selectOptions = Object.entries(options).reduce((acc, [key, val]) => {
acc.push({ label: key, value: val });
return acc;
}, []);
const handleChange: ReactSelectOnChangeFn = isMulti const SingleSelect: FC<SelectProps> = ({ name, value, options, onChange }) => {
? (values: OptionsItem[]) => onChange(name, values && values.map((item) => item.value)) const handleChange = (e: ChangeEvent<HTMLSelectElement>) => {
: (e: OptionsItem) => onChange(name, e.value); onChange(name, options[e.currentTarget.value]);
};
const selection = selectedKey(value, options);
return ( return (
<OptionsSelect <OptionsSelect value={selection} onChange={handleChange}>
defaultValue={value} {Object.keys(options).map((key) => (
options={selectOptions} <option>{key}</option>
isMulti={isMulti} ))}
onChange={handleChange} </OptionsSelect>
/>
); );
}; };
const MultiSelect: FC<SelectProps> = ({ name, value, options, onChange }) => {
const handleChange = (e: ChangeEvent<HTMLSelectElement>) => {
const selection = Array.from(e.currentTarget.options)
.filter((option) => option.selected)
.map((option) => option.value);
onChange(name, selectedValues(selection, options));
};
const selection = selectedKeys(value, options);
return (
<OptionsSelect multiple value={selection} onChange={handleChange}>
{Object.keys(options).map((key) => (
<option>{key}</option>
))}
</OptionsSelect>
);
};
export const SelectControl: FC<SelectProps> = (props) =>
// eslint-disable-next-line react/destructuring-assignment
props.isMulti ? <MultiSelect {...props} /> : <SingleSelect {...props} />;

View File

@ -0,0 +1,14 @@
import { OptionsObject } from '../types';
export const selectedKey = (value: any, options: OptionsObject) => {
const entry = Object.entries(options).find(([_key, val]) => val === value);
return entry ? entry[0] : undefined;
};
export const selectedKeys = (value: any[], options: OptionsObject) =>
Object.entries(options)
.filter((entry) => value.includes(entry[1]))
.map((entry) => entry[0]);
export const selectedValues = (keys: string[], options: OptionsObject) =>
keys.map((key) => options[key]);