mirror of
https://github.com/storybookjs/storybook.git
synced 2025-04-08 02:21:48 +08:00
Controls: Remove react-select and fix initialization logic
This commit is contained in:
parent
29a13f6657
commit
e11b64df92
@ -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>
|
||||||
|
|
||||||
|
@ -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"
|
||||||
|
@ -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>
|
||||||
|
@ -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);
|
||||||
|
@ -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;
|
||||||
}, {});
|
}, {});
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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} />;
|
||||||
|
14
lib/components/src/controls/options/helpers.ts
Normal file
14
lib/components/src/controls/options/helpers.ts
Normal 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]);
|
Loading…
x
Reference in New Issue
Block a user