From d595eafc16ad41a435a5970fdd6df1956ff8eb71 Mon Sep 17 00:00:00 2001 From: Benny Date: Tue, 3 May 2022 16:58:22 +0800 Subject: [PATCH 1/6] feat: implement RadioSelectList and RadioSelectOption --- packages/core/src/index.js | 2 + packages/form/src/RadioSelectList.js | 37 ++++++++++ packages/form/src/RadioSelectOption.js | 68 +++++++++++++++++++ packages/form/src/SelectList.js | 12 +++- packages/form/src/utils/parseSelectOptions.js | 9 ++- .../examples/form/RadioSelectList.stories.js | 52 ++++++++++++++ 6 files changed, 176 insertions(+), 4 deletions(-) create mode 100644 packages/form/src/RadioSelectList.js create mode 100644 packages/form/src/RadioSelectOption.js create mode 100644 packages/storybook/examples/form/RadioSelectList.stories.js diff --git a/packages/core/src/index.js b/packages/core/src/index.js index d1079471..b354e75b 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -27,6 +27,7 @@ import IconCheckbox from './IconCheckbox'; import Switch from './Switch'; import TextInput from './TextInput'; import SearchInput from './SearchInput'; +import Radio from './Radio'; // Containers import InfiniteScroll from './InfiniteScroll'; @@ -68,6 +69,7 @@ export { Switch, TextInput, SearchInput, + Radio, InfiniteScroll, HeaderRow, diff --git a/packages/form/src/RadioSelectList.js b/packages/form/src/RadioSelectList.js new file mode 100644 index 00000000..bb156f82 --- /dev/null +++ b/packages/form/src/RadioSelectList.js @@ -0,0 +1,37 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { valueType } from './RadioSelectOption'; +import SelectList from './SelectList'; + + +export default function RadioSelectList({ + // TODO: omit + ...otherProps +}) { + return ( + + ); +} + +RadioSelectList.propTypes = { + value: valueType, + defaultValue: valueType, + onChange: PropTypes.func, + title: PropTypes.string, + desc: PropTypes.node, +}; + +RadioSelectList.defaultProps = { + value: undefined, + defaultValue: undefined, + onChange: () => {}, + title: undefined, + desc: undefined, +}; diff --git a/packages/form/src/RadioSelectOption.js b/packages/form/src/RadioSelectOption.js new file mode 100644 index 00000000..e73b513f --- /dev/null +++ b/packages/form/src/RadioSelectOption.js @@ -0,0 +1,68 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { + Radio, + ListRow, +} from '@ichef/gypcrete'; + +export const valueType = PropTypes.oneOfType([ + PropTypes.string, + PropTypes.number, + PropTypes.bool, +]); + +export const TYPE_SYMBOL = Symbol('RadioSelectOption'); + +function RadioSelectOption({ + label, + desc, + value, + avatar, + readOnly, + checked, + onChange, + ...checkboxProps +}) { + const handleRadioChange = (event) => { + onChange(value, event.target.checked); + }; + + return ( + + + + ); +} + +RadioSelectOption.propTypes = { + label: PropTypes.node.isRequired, + desc: PropTypes.node, + value: valueType, + avatar: PropTypes.node, + readOnly: PropTypes.bool, + // Set by + checked: PropTypes.bool, + onChange: PropTypes.func, +}; + +RadioSelectOption.defaultProps = { + desc: null, + value: null, + avatar: null, + readOnly: false, + checked: false, + onChange: () => {}, +}; + +RadioSelectOption.typeSymbol = TYPE_SYMBOL; + +export default RadioSelectOption; diff --git a/packages/form/src/SelectList.js b/packages/form/src/SelectList.js index db29df64..066421e0 100644 --- a/packages/form/src/SelectList.js +++ b/packages/form/src/SelectList.js @@ -11,9 +11,13 @@ import getRemainingProps from '@ichef/gypcrete/lib/utils/getRemainingProps'; import Option, { valueType, - TYPE_SYMBOL as OPTION_TYPE_SYMBOL, + TYPE_SYMBOL as CHECKBOX_OPTION_TYPE_SYMBOL, } from './SelectOption'; +import { + TYPE_SYMBOL as RADIO_OPTION_TYPE_SYMBOL, +} from './RadioSelectOption'; + import parseSelectOptions from './utils/parseSelectOptions'; import getElementTypeSymbol from './utils/getElementTypeSymbol'; @@ -193,7 +197,11 @@ class SelectList extends PureComponent { const { checkedState } = this.state; return React.Children.map(children, (child) => { - if (getElementTypeSymbol(child) === OPTION_TYPE_SYMBOL) { + const elementTypeSymbol = getElementTypeSymbol(child); + if ( + elementTypeSymbol === CHECKBOX_OPTION_TYPE_SYMBOL + || elementTypeSymbol === RADIO_OPTION_TYPE_SYMBOL + ) { return React.cloneElement(child, { checked: checkedState.get(child.props.value), onChange: this.handleOptionChange, diff --git a/packages/form/src/utils/parseSelectOptions.js b/packages/form/src/utils/parseSelectOptions.js index 2d1729e1..0b0b16d6 100644 --- a/packages/form/src/utils/parseSelectOptions.js +++ b/packages/form/src/utils/parseSelectOptions.js @@ -1,5 +1,6 @@ import { Fragment } from 'react'; -import { TYPE_SYMBOL } from '../SelectOption'; +import { TYPE_SYMBOL as CHECKBOX_OPTION_TYPE_SYMBOL } from '../SelectOption'; +import { TYPE_SYMBOL as RADIO_OPTION_TYPE_SYMBOL } from '../RadioSelectOption'; import getElementTypeSymbol from './getElementTypeSymbol'; @@ -13,7 +14,11 @@ export default function parseSelectOptions(children) { const childArray = Array.isArray(children) ? children : [children].filter(item => item); const results = childArray.map((child) => { - if (getElementTypeSymbol(child) === TYPE_SYMBOL) { + const elementTypeSymbol = getElementTypeSymbol(child); + if ( + elementTypeSymbol === CHECKBOX_OPTION_TYPE_SYMBOL + || elementTypeSymbol === RADIO_OPTION_TYPE_SYMBOL + ) { return child.props; } diff --git a/packages/storybook/examples/form/RadioSelectList.stories.js b/packages/storybook/examples/form/RadioSelectList.stories.js new file mode 100644 index 00000000..9f82e404 --- /dev/null +++ b/packages/storybook/examples/form/RadioSelectList.stories.js @@ -0,0 +1,52 @@ +import React from 'react'; +import { action } from '@storybook/addon-actions'; + +import RadioSelectList from '@ichef/gypcrete-form/src/RadioSelectList'; +import RadioSelectOption from '@ichef/gypcrete-form/src/RadioSelectOption'; + +export default { + title: '@ichef/gypcrete-form|RadioSelectList', + component: RadioSelectList, + subcomponents: { RadioSelectOption }, +}; + +export const singleUncontrolled = () => ( + + + + + +); +singleUncontrolled.story = { + name: 'Single-value (uncontrolled)', +}; + +export const singleControlled = () => ( + + + + + +); +singleControlled.story = { + name: 'Single-value (controlled)', + parameters: { + docs: { + storyDescription: 'Observe its onChange() should be firing with user-clicked option', + }, + }, +}; + +export const multipleWithReadOnly = () => ( + + + + + +); +multipleWithReadOnly.story = { + name: 'With Read-only options', +}; From 984dedccdd7a0655de582ccde184000b5c94fd02 Mon Sep 17 00:00:00 2001 From: Benny Date: Tue, 3 May 2022 21:24:46 +0800 Subject: [PATCH 2/6] feat: add props for RadioSelectList --- packages/form/src/RadioSelectList.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/packages/form/src/RadioSelectList.js b/packages/form/src/RadioSelectList.js index bb156f82..9f81133f 100644 --- a/packages/form/src/RadioSelectList.js +++ b/packages/form/src/RadioSelectList.js @@ -4,18 +4,27 @@ import PropTypes from 'prop-types'; import { valueType } from './RadioSelectOption'; import SelectList from './SelectList'; - export default function RadioSelectList({ - // TODO: omit + value, + defaultValue, + onChange, + title, + desc, ...otherProps }) { return ( ); } From 53f3719c978a3fae446fa7afd2c6059ae00e1c2b Mon Sep 17 00:00:00 2001 From: Benny Date: Tue, 3 May 2022 21:34:21 +0800 Subject: [PATCH 3/6] fix: fix props name --- packages/form/src/RadioSelectOption.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/form/src/RadioSelectOption.js b/packages/form/src/RadioSelectOption.js index e73b513f..887c9cbd 100644 --- a/packages/form/src/RadioSelectOption.js +++ b/packages/form/src/RadioSelectOption.js @@ -22,7 +22,7 @@ function RadioSelectOption({ readOnly, checked, onChange, - ...checkboxProps + ...radioProps }) { const handleRadioChange = (event) => { onChange(value, event.target.checked); @@ -37,7 +37,7 @@ function RadioSelectOption({ aside={desc} avatar={avatar} onChange={handleRadioChange} - {...checkboxProps} + {...radioProps} /> ); @@ -49,7 +49,7 @@ RadioSelectOption.propTypes = { value: valueType, avatar: PropTypes.node, readOnly: PropTypes.bool, - // Set by + // Set by checked: PropTypes.bool, onChange: PropTypes.func, }; From f7a5b090479162eeedc718a18197e1c13f12f32b Mon Sep 17 00:00:00 2001 From: Benny Date: Tue, 3 May 2022 21:37:49 +0800 Subject: [PATCH 4/6] docs: update CHANGELOG --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c45bf1e2..b24dd324 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,13 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Added - [Core] Add new icon: Location. (#333) +- [Core] Add Radio component (#338, #339) +- [Form] Add RadioSelectList and RadioSelectOption (#340) + +### Breaking +- [Core] Rename radio-empty.svg to checkbox-empty.svg (#337) +- [Core] Rename radio-half.svg to checkbox-half.svg (#337) +- [Core] Rename radio-selected.svg to checkbox-selected.svg (#337) ## [4.4.0] From ba3c3c496de604e574aaa4fd0ff6b477a20d6fd9 Mon Sep 17 00:00:00 2001 From: Benny Date: Tue, 3 May 2022 21:45:33 +0800 Subject: [PATCH 5/6] test(parseSelectOptions): add unit test for RadioSelectOption --- packages/form/src/SelectList.js | 1 - .../__tests__/parseSelectOptions.test.js | 20 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/packages/form/src/SelectList.js b/packages/form/src/SelectList.js index 066421e0..c675c6df 100644 --- a/packages/form/src/SelectList.js +++ b/packages/form/src/SelectList.js @@ -13,7 +13,6 @@ import Option, { valueType, TYPE_SYMBOL as CHECKBOX_OPTION_TYPE_SYMBOL, } from './SelectOption'; - import { TYPE_SYMBOL as RADIO_OPTION_TYPE_SYMBOL, } from './RadioSelectOption'; diff --git a/packages/form/src/utils/__tests__/parseSelectOptions.test.js b/packages/form/src/utils/__tests__/parseSelectOptions.test.js index ea0cd629..da0bdcaa 100644 --- a/packages/form/src/utils/__tests__/parseSelectOptions.test.js +++ b/packages/form/src/utils/__tests__/parseSelectOptions.test.js @@ -1,6 +1,7 @@ import React from 'react'; import SelectOption from 'src/SelectOption'; +import RadioSelectOption from 'src/RadioSelectOption'; import parseSelectOptions from '../parseSelectOptions'; it('reads options from React children of s', () => { @@ -22,6 +23,25 @@ it('reads options from React children of s', () => { ]); }); +it('reads options from React children of s', () => { + const singleOption = ; + + expect(parseSelectOptions(singleOption)).toMatchObject([ + { label: 'Foo', value: 'foo' }, + ]); + + const multipleOptions = [ + , + , + , + ]; + expect(parseSelectOptions(multipleOptions)).toMatchObject([ + { label: 'Foo', value: 'foo', checked: true }, + { label: 'Bar', value: 'bar', readOnly: true }, + { label: 'Meh', value: 'meh' }, + ]); +}); + // #TODO: Add warning for children other than it('ignores children that are not ', () => { const children = [ From ca5074db52607bea26e7016fdbaf05829ef2f09f Mon Sep 17 00:00:00 2001 From: Benny Date: Thu, 5 May 2022 09:30:19 +0800 Subject: [PATCH 6/6] docs: update comment for typeSymbol --- packages/form/src/RadioSelectOption.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/form/src/RadioSelectOption.js b/packages/form/src/RadioSelectOption.js index 887c9cbd..a9b5310d 100644 --- a/packages/form/src/RadioSelectOption.js +++ b/packages/form/src/RadioSelectOption.js @@ -63,6 +63,16 @@ RadioSelectOption.defaultProps = { onChange: () => {}, }; +/** + * The same reason in the SelectOption.js. + * + * `react-hot-loader` v4 wraps every single component with a proxy for its + * internal uses. This breaks the comparison of this component and type from + * any React.Element, because the later will always be a hot-loader proxy. + * + * I'm trying to add a new way for comparison so we can still be sure if an + * element is created from . + */ RadioSelectOption.typeSymbol = TYPE_SYMBOL; export default RadioSelectOption;