Skip to content

Commit

Permalink
Merge pull request #11024 from storybookjs/10855-args-default-values
Browse files Browse the repository at this point in the history
Addon-controls: Fix initialization logic; remove react-select
  • Loading branch information
shilman authored Jun 3, 2020
2 parents e1880cc + 0443717 commit 0cd00c7
Show file tree
Hide file tree
Showing 9 changed files with 138 additions and 84 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const argsTableProps = (component: Component) => {

const ArgsStory = ({ component }: any) => {
const { rows } = argsTableProps(component);
const initialArgs = mapValues(rows, () => null) as Args;
const initialArgs = mapValues(rows, (argType) => argType.defaultValue) as Args;

const [args, setArgs] = useState(initialArgs);
return (
Expand Down
62 changes: 40 additions & 22 deletions examples/official-storybook/stories/addon-docs/props.stories.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,14 @@ import { MemoButton } from '../../components/MemoButton';
parameters={{ controls: { expanded: false } }}
/>

export const FooBar = ({ foo, bar, baz } = {}) => (
export const ArgsDisplay = (args = {}) => (
<table>
<tr>
<td>Foo</td>
<td>{foo && foo.toString()}</td>
</tr>
<tr>
<td>Bar</td>
<td>{bar}</td>
</tr>
<tr>
<td>Baz</td>
<td>{baz && baz.toString()}</td>
</tr>
{Object.entries(args).map(([key, val]) => (
<tr>
<td>{key}</td>
<td>{val && val.toString()}</td>
</tr>
))}
</table>
);

Expand All @@ -34,21 +28,45 @@ export const FooBar = ({ foo, bar, baz } = {}) => (
<Story
name="ArgTypes"
args={{
foo: false,
bar: '',
baz: ['a', 'b'],
boolArg: true,
stringArg: 'overwritten',
arrayArg: ['a', 'b'],
}}
argTypes={{
foo: { name: 'foo', type: { name: 'boolean' }, description: 'foo description' },
bar: { name: 'bar', type: { name: 'string' }, description: 'bar description' },
baz: {
name: 'baz',
boolArg: { name: 'boolArg', type: { name: 'boolean' }, description: 'bool description' },
stringArg: {
name: 'stringArg',
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' } },
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>
</Preview>

Expand All @@ -68,7 +86,7 @@ export const FooBar = ({ foo, bar, baz } = {}) => (
bar: '',
}}
>
{(args) => <FooBar {...args} />}
{(args) => <ArgsDisplay {...args} />}
</Story>
</Preview>

Expand Down
1 change: 0 additions & 1 deletion lib/components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@
"react-dom": "^16.8.3",
"react-helmet-async": "^1.0.2",
"react-popper-tooltip": "^2.11.0",
"react-select": "^3.0.8",
"react-syntax-highlighter": "^12.2.1",
"react-textarea-autosize": "^7.1.0",
"ts-dedent": "^1.1.1"
Expand Down
22 changes: 11 additions & 11 deletions lib/components/src/controls/options/Checkbox.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { FC, ChangeEvent, useState } from 'react';
import { styled } from '@storybook/theming';
import { ControlProps, OptionsMultiSelection, NormalizedOptionsConfig } from '../types';
import { selectedKeys, selectedValues } from './helpers';

const CheckboxesWrapper = styled.div<{ isInline: boolean }>(({ isInline }) =>
isInline
Expand Down Expand Up @@ -36,36 +37,35 @@ export const CheckboxControl: FC<CheckboxProps> = ({
onChange,
isInline,
}) => {
const [selected, setSelected] = useState(value || []);
const initial = selectedKeys(value, options);
const [selected, setSelected] = useState(initial);

const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
const option = (e.target as HTMLInputElement).value;
const newVal = [...selected];
if (newVal.includes(option)) {
newVal.splice(newVal.indexOf(option), 1);
const updated = [...selected];
if (updated.includes(option)) {
updated.splice(updated.indexOf(option), 1);
} else {
newVal.push(option);
updated.push(option);
}
onChange(name, newVal);
setSelected(newVal);
onChange(name, selectedValues(updated, options));
setSelected(updated);
};

return (
<CheckboxFieldset>
<CheckboxesWrapper isInline={isInline}>
{Object.keys(options).map((key: string) => {
const id = `${name}-${key}`;
const optionValue = options[key];

return (
<div key={id}>
<input
type="checkbox"
id={id}
name={name}
value={optionValue}
value={key}
onChange={handleChange}
checked={selected.includes(optionValue)}
checked={selected.includes(key)}
/>
<CheckboxLabel htmlFor={id}>{key}</CheckboxLabel>
</div>
Expand Down
32 changes: 16 additions & 16 deletions lib/components/src/controls/options/Options.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,18 @@ const objectOptions = {
B: { id: 'Bat' },
C: { id: 'Cat' },
};
const emptyOptions = null;

const optionsHelper = (options, type) => {
const [value, setValue] = useState([]);
const optionsHelper = (options, type, isMulti) => {
const initial = Array.isArray(options) ? options[1] : options.B;
const [value, setValue] = useState(isMulti ? [initial] : initial);
return (
<>
<OptionsControl
name="options"
options={options}
value={value}
type={type}
onChange={(name, newVal) => setValue(newVal)}
onChange={(_name, newVal) => setValue(newVal)}
/>
{value && Array.isArray(value) ? (
// eslint-disable-next-line react/no-array-index-key
Expand All @@ -36,19 +36,19 @@ const optionsHelper = (options, type) => {
};

// Check
export const CheckArray = () => optionsHelper(arrayOptions, 'check');
export const InlineCheckArray = () => optionsHelper(arrayOptions, 'inline-check');
export const CheckObject = () => optionsHelper(objectOptions, 'check');
export const InlineCheckObject = () => optionsHelper(objectOptions, 'inline-check');
export const CheckArray = () => optionsHelper(arrayOptions, 'check', true);
export const InlineCheckArray = () => optionsHelper(arrayOptions, 'inline-check', true);
export const CheckObject = () => optionsHelper(objectOptions, 'check', true);
export const InlineCheckObject = () => optionsHelper(objectOptions, 'inline-check', true);

// Radio
export const ArrayRadio = () => optionsHelper(arrayOptions, 'radio');
export const ArrayInlineRadio = () => optionsHelper(arrayOptions, 'inline-radio');
export const ObjectRadio = () => optionsHelper(objectOptions, 'radio');
export const ObjectInlineRadio = () => optionsHelper(objectOptions, 'inline-radio');
export const ArrayRadio = () => optionsHelper(arrayOptions, 'radio', false);
export const ArrayInlineRadio = () => optionsHelper(arrayOptions, 'inline-radio', false);
export const ObjectRadio = () => optionsHelper(objectOptions, 'radio', false);
export const ObjectInlineRadio = () => optionsHelper(objectOptions, 'inline-radio', false);

// Select
export const ArraySelect = () => optionsHelper(arrayOptions, 'select');
export const ArrayMultiSelect = () => optionsHelper(arrayOptions, 'multi-select');
export const ObjectSelect = () => optionsHelper(objectOptions, 'select');
export const ObjectMultiSelect = () => optionsHelper(objectOptions, 'multi-select');
export const ArraySelect = () => optionsHelper(arrayOptions, 'select', false);
export const ArrayMultiSelect = () => optionsHelper(arrayOptions, 'multi-select', true);
export const ObjectSelect = () => optionsHelper(objectOptions, 'select', false);
export const ObjectMultiSelect = () => optionsHelper(objectOptions, 'multi-select', true);
10 changes: 9 additions & 1 deletion lib/components/src/controls/options/Options.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,18 @@ import { RadioControl } from './Radio';
import { SelectControl } from './Select';
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) => {
if (Array.isArray(options)) {
return options.reduce((acc, item) => {
acc[item] = item;
acc[item.toString()] = item;
return acc;
}, {});
}
Expand Down
11 changes: 6 additions & 5 deletions lib/components/src/controls/options/Radio.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { FC, Validator } from 'react';
import React, { FC } from 'react';
import { styled } from '@storybook/theming';
import { ControlProps, OptionsSingleSelection, NormalizedOptionsConfig } from '../types';
import { selectedKey } from './helpers';

const RadiosWrapper = styled.div<{ isInline: boolean }>(({ isInline }) =>
isInline
Expand All @@ -24,20 +25,20 @@ const RadioLabel = styled.label({
type RadioConfig = NormalizedOptionsConfig & { isInline: boolean };
type RadioProps = ControlProps<OptionsSingleSelection> & RadioConfig;
export const RadioControl: FC<RadioProps> = ({ name, options, value, onChange, isInline }) => {
const selection = selectedKey(value, options);
return (
<RadiosWrapper isInline={isInline}>
{Object.keys(options).map((key) => {
const id = `${name}-${key}`;
const optionValue = options[key];
return (
<div key={id}>
<input
type="radio"
id={id}
name={name}
value={optionValue || undefined}
onChange={(e) => onChange(name, e.target.value)}
checked={optionValue === value}
value={key}
onChange={(e) => onChange(name, options[e.currentTarget.value])}
checked={key === selection}
/>
<RadioLabel htmlFor={id}>{key}</RadioLabel>
</div>
Expand Down
68 changes: 41 additions & 27 deletions lib/components/src/controls/options/Select.tsx
Original file line number Diff line number Diff line change
@@ -1,41 +1,55 @@
import React, { FC } from 'react';
import ReactSelect from 'react-select';
import React, { FC, ChangeEvent } from 'react';
import { styled } from '@storybook/theming';
import { ControlProps, OptionsSelection, NormalizedOptionsConfig } from '../types';
import { selectedKey, selectedKeys, selectedValues } from './helpers';

// TODO: Apply the Storybook theme to react-select
const OptionsSelect = styled(ReactSelect)({
const OptionsSelect = styled.select({
width: '100%',
maxWidth: '300px',
color: 'black',
});

interface OptionsItem {
value: any;
label: string;
}
type ReactSelectOnChangeFn = { (v: OptionsItem): void } | { (v: OptionsItem[]): void };

type SelectConfig = NormalizedOptionsConfig & { isMulti: boolean };
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
? (values: OptionsItem[]) => onChange(name, values && values.map((item) => item.value))
: (e: OptionsItem) => onChange(name, e.value);

const NO_SELECTION = 'Select...';

const SingleSelect: FC<SelectProps> = ({ name, value, options, onChange }) => {
const handleChange = (e: ChangeEvent<HTMLSelectElement>) => {
onChange(name, options[e.currentTarget.value]);
};
const selection = selectedKey(value, options) || NO_SELECTION;

return (
<OptionsSelect
defaultValue={value}
options={selectOptions}
isMulti={isMulti}
onChange={handleChange}
/>
<OptionsSelect value={selection} onChange={handleChange}>
<option key="no-selection" disabled>
{NO_SELECTION}
</option>
{Object.keys(options).map((key) => (
<option key={key}>{key}</option>
))}
</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={key}>{key}</option>
))}
</OptionsSelect>
);
};

export const SelectControl: FC<SelectProps> = (props) =>
// eslint-disable-next-line react/destructuring-assignment
props.isMulti ? <MultiSelect {...props} /> : <SingleSelect {...props} />;
14 changes: 14 additions & 0 deletions lib/components/src/controls/options/helpers.ts
Original file line number Diff line number Diff line change
@@ -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]);

0 comments on commit 0cd00c7

Please sign in to comment.