diff --git a/docs/components/Layout/index.jsx b/docs/components/Layout/index.jsx
index 3bd7cf8bf..6d6767629 100644
--- a/docs/components/Layout/index.jsx
+++ b/docs/components/Layout/index.jsx
@@ -16,6 +16,7 @@ import AlertExample from '../../examples/AlertExample';
import CheckboxExample from '../../examples/CheckboxExample';
import CheckboxGroupExample from '../../examples/CheckboxGroupExample';
import RadioExample from '../../examples/RadioExample';
+import RadioGroupExample from '../../examples/RadioGroupExample';
import SelectExample from '../../examples/SelectExample';
import DatePickerExample from '../../examples/DatePickerExample';
import BorderedWellExample from '../../examples/BorderedWellExample';
@@ -178,6 +179,7 @@ class PageLayout extends React.Component {
+
diff --git a/docs/examples/RadioExample.jsx b/docs/examples/RadioExample.jsx
index 4e4f0672f..1ecd58483 100644
--- a/docs/examples/RadioExample.jsx
+++ b/docs/examples/RadioExample.jsx
@@ -1,52 +1,78 @@
import React from 'react';
import Example from '../components/Example';
-import { Radio, RadioGroup } from '../../src';
+import Radio from 'adslot-ui/Radio';
class RadioExample extends React.PureComponent {
+ onChange(event) {
+ _.noop();
+ }
+
render() {
return (
-
-
-
-
+
);
}
}
const exampleProps = {
componentName: 'Radio',
- designNotes: (
-
- Radio buttons used for making a single selection from multiple options. Only
- one selection can ever be made from the radio button group at a time.
-
- ),
- notes: (
-
- See React iCheck Documentation
-
- ),
- exampleCodeSnippet: `
-
-
-
- `,
+ notes: '',
+ exampleCodeSnippet: ` `,
propTypeSectionArray: [
{
propTypes: [
+ {
+ propType: 'id',
+ type: 'string',
+ },
+ {
+ propType: 'className',
+ type: 'string',
+ note: 'This class will be applied to the input element',
+ },
+ {
+ propType: 'name',
+ type: 'string',
+ },
{
propType: 'label',
- type: 'node',
- note: 'Usually fine to rely on a string but can pass HTML e.g. for a url.',
+ type: 'string',
},
{
- propType: 'value',
+ propType: 'dts',
type: 'string',
+ note: 'render `data-test-selector` onto the component. It can be useful for testing.',
+ },
+ {
+ propType: 'disabled',
+ type: 'bool',
+ defaultValue: false
,
+ },
+ {
+ propType: 'checked',
+ type: 'bool',
+ defaultValue: false
,
},
{
propType: 'onChange',
type: 'func',
},
+ {
+ propType: 'value',
+ type: 'string',
+ },
],
},
],
diff --git a/docs/examples/RadioGroupExample.jsx b/docs/examples/RadioGroupExample.jsx
new file mode 100644
index 000000000..c1fce4fb3
--- /dev/null
+++ b/docs/examples/RadioGroupExample.jsx
@@ -0,0 +1,89 @@
+import React from 'react';
+import Example from '../components/Example';
+import RadioGroup from 'adslot-ui/RadioGroup';
+import Radio from 'adslot-ui/Radio';
+
+class RadioGroupExample extends React.PureComponent {
+ onChangeGroup(event) {
+ _.noop();
+ }
+
+ onChangeIndividual(event) {
+ _.noop();
+ }
+
+ render() {
+ return (
+
+
+
+
+
+ );
+ }
+}
+
+const exampleProps = {
+ componentName: 'RadioGroup',
+ designNotes: (
+
+ Radio buttons used for making a single selection from multiple options. Only
+ one selection can ever be made from the radio button group at a time.
+
+ ),
+ notes: '',
+ exampleCodeSnippet: `
+
+
+
+ `,
+ propTypeSectionArray: [
+ {
+ propTypes: [
+ {
+ propType: 'name',
+ type: 'string',
+ note: (
+
+ Required. All Radio buttons within this group will have the same name
+
+ ),
+ },
+ {
+ propType: 'value',
+ type: 'string',
+ note: 'value of the selected radio button',
+ },
+ {
+ propType: 'onChange',
+ type: 'func',
+ note: 'Triggers when selection changes.',
+ },
+ {
+ propType: 'children',
+ type: 'node',
+ note: `Should be an array of components`,
+ },
+ {
+ propType: 'className',
+ type: 'string',
+ },
+ {
+ propType: 'dts',
+ type: 'string',
+ note: 'render `data-test-selector` onto the component. It can be useful for testing.',
+ },
+ {
+ propType: 'id',
+ type: 'string',
+ },
+ ],
+ },
+ ],
+};
+
+export default () => (
+
+
+
+);
diff --git a/src/components/adslot-ui/Radio/index.jsx b/src/components/adslot-ui/Radio/index.jsx
new file mode 100644
index 000000000..7ddf57e82
--- /dev/null
+++ b/src/components/adslot-ui/Radio/index.jsx
@@ -0,0 +1,64 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { expandDts } from 'lib/utils';
+
+class RadioButton extends React.Component {
+ static getDerivedStateFromProps(newProps, prevState) {
+ return newProps.checked === prevState.checked ? null : { checked: newProps.checked };
+ }
+
+ constructor(props) {
+ super(props);
+ this.state = { checked: props.checked };
+
+ this.onChangeDefault = this.onChangeDefault.bind(this);
+ }
+
+ onChangeDefault(event) {
+ this.setState({ checked: Boolean(event.target.checked) });
+ if (this.props.onChange) {
+ this.props.onChange(event);
+ }
+ }
+
+ render() {
+ const { name, className, label, dts, disabled, id, value } = this.props;
+
+ const radioInputProps = {
+ type: 'radio',
+ name,
+ checked: this.state.checked,
+ disabled,
+ onChange: this.onChangeDefault,
+ value,
+ id,
+ className,
+ };
+
+ return (
+
+
+ {label ? {label} : null}
+
+ );
+ }
+}
+
+RadioButton.propTypes = {
+ id: PropTypes.string,
+ className: PropTypes.string,
+ name: PropTypes.string,
+ label: PropTypes.node,
+ dts: PropTypes.string,
+ disabled: PropTypes.bool,
+ checked: PropTypes.bool,
+ onChange: PropTypes.func,
+ value: PropTypes.string,
+};
+
+RadioButton.defaultProps = {
+ disabled: false,
+ checked: false,
+};
+
+export default RadioButton;
diff --git a/src/components/adslot-ui/Radio/index.spec.jsx b/src/components/adslot-ui/Radio/index.spec.jsx
new file mode 100644
index 000000000..45550e94f
--- /dev/null
+++ b/src/components/adslot-ui/Radio/index.spec.jsx
@@ -0,0 +1,81 @@
+import _ from 'lodash';
+import React from 'react';
+import { shallow } from 'enzyme';
+import sinon from 'sinon';
+import Radio from '.';
+
+describe(' ', () => {
+ let props;
+
+ beforeEach(() => {
+ props = {
+ name: 'radio-name',
+ value: 'radio-value',
+ label: 'Radio 1',
+ dts: 'radio-dts',
+ id: 'radio-id',
+ className: 'radio-class',
+ disabled: false,
+ checked: false,
+ onChange: sinon.spy(),
+ };
+ });
+
+ it('should render with props', () => {
+ const component = shallow( );
+ expect(component.find('input[type="radio"]')).to.have.length(1);
+ expect(component.text()).to.equal('Radio 1');
+ expect(component.find('[name="radio-name"]')).to.have.length(1);
+ expect(component.find('[value="radio-value"]')).to.have.length(1);
+ expect(component.find('[data-test-selector="radio-dts"]')).to.have.length(1);
+ });
+
+ it('should not render label if props.label is undefined', () => {
+ delete props.label;
+ const component = shallow( );
+ expect(component.text()).to.equal('');
+ });
+
+ it('should trigger state change and `props.onChange` when change event is triggered', () => {
+ const component = shallow( );
+ const event = { target: { checked: true } };
+
+ expect(component.state('checked')).to.equal(false);
+
+ component.find('input').simulate('change', event);
+ expect(component.state('checked')).to.equal(true);
+ expect(props.onChange.calledOnce).to.equal(true);
+ });
+
+ it('should still trigger state change when `props.onChange` is not present', () => {
+ delete props.onChange;
+ const component = shallow( );
+ const event = { target: { checked: true } };
+
+ expect(component.state('checked')).to.equal(false);
+
+ component.find('input').simulate('change', event);
+ expect(component.state('checked')).to.equal(true);
+ });
+
+ it('should override state value when `prop.value` changes', () => {
+ const component = shallow( );
+ expect(component.state('checked')).to.equal(false);
+
+ props.checked = true;
+ component.setProps(props);
+ expect(component.state('checked')).to.equal(true);
+ });
+
+ it('should NOT override state value when other props change', () => {
+ const component = shallow( );
+ expect(component.state('checked')).to.equal(false);
+
+ _.assign(props, {
+ name: 'some-other-name',
+ label: 'New Label',
+ });
+ component.setProps(props);
+ expect(component.state('checked')).to.equal(false);
+ });
+});
diff --git a/src/components/adslot-ui/RadioGroup/index.jsx b/src/components/adslot-ui/RadioGroup/index.jsx
new file mode 100644
index 000000000..afd778d91
--- /dev/null
+++ b/src/components/adslot-ui/RadioGroup/index.jsx
@@ -0,0 +1,71 @@
+import _ from 'lodash';
+import React from 'react';
+import PropTypes from 'prop-types';
+import { expandDts } from 'lib/utils';
+
+class RadioGroup extends React.Component {
+ static getDerivedStateFromProps(nextProps, prevState) {
+ return nextProps.value === prevState.value ? null : { value: nextProps.value };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ value: props.value,
+ };
+
+ this.onChangeDefault = this.onChangeDefault.bind(this);
+ this.renderChildren = this.renderChildren.bind(this);
+ }
+
+ onChangeDefault(event) {
+ this.setState({ value: event.target.value });
+ if (this.props.onChange) {
+ this.props.onChange(event);
+ }
+ }
+
+ renderChildren() {
+ return React.Children.map(this.props.children, child => {
+ const childProps = _.assign({}, child.props, {
+ name: this.props.name,
+ checked: this.state.value === child.props.value,
+ onChange: (...args) => {
+ if (child.props.onChange) child.props.onChange(...args);
+ this.onChangeDefault(...args);
+ },
+ });
+
+ return React.cloneElement(child, childProps);
+ });
+ }
+
+ render() {
+ const { dts, className, id } = this.props;
+ const props = _.pickBy({
+ id,
+ className: _(['radio-group-component', className])
+ .compact()
+ .join(' '),
+ });
+
+ return (
+
+ {this.renderChildren()}
+
+ );
+ }
+}
+
+RadioGroup.propTypes = {
+ name: PropTypes.string.isRequired,
+ value: PropTypes.string,
+ onChange: PropTypes.func,
+ children: PropTypes.node,
+ className: PropTypes.string,
+ dts: PropTypes.string,
+ id: PropTypes.string,
+};
+
+export default RadioGroup;
diff --git a/src/components/adslot-ui/RadioGroup/index.spec.jsx b/src/components/adslot-ui/RadioGroup/index.spec.jsx
new file mode 100644
index 000000000..3d8e062a7
--- /dev/null
+++ b/src/components/adslot-ui/RadioGroup/index.spec.jsx
@@ -0,0 +1,119 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import sinon from 'sinon';
+import Radio from '../Radio';
+import RadioGroup from '.';
+
+describe(' ', () => {
+ let props;
+
+ beforeEach(() => {
+ props = {
+ id: 'radio-group-id',
+ className: 'radio-group-custom-class',
+ name: 'hobbies',
+ value: 'badminton',
+ onChange: sinon.spy(),
+ dts: 'radio-group-dts',
+ };
+ });
+
+ it('should render with props', () => {
+ const component = shallow(
+
+
+
+
+
+ );
+
+ expect(component.find(Radio)).to.have.length(3);
+ expect(component.find('#radio-group-id')).to.have.length(1);
+ expect(component.find('.radio-group-custom-class')).to.have.length(1);
+ expect(component.find('[data-test-selector="radio-group-dts"]')).to.have.length(1);
+ });
+
+ it('should update state and trigger props.onChange when selection changes', () => {
+ const component = shallow(
+
+
+
+
+
+ );
+
+ expect(component.state('value')).to.equal('badminton');
+ component
+ .find(Radio)
+ .at(0)
+ .simulate('change', { target: { value: 'swimming' } });
+ expect(component.state('value')).to.equal('swimming');
+ expect(props.onChange.calledOnce).to.equal(true);
+ });
+
+ it('should still update state when props.onChange is not available', () => {
+ delete props.onChange;
+ const component = shallow(
+
+
+
+
+
+ );
+
+ expect(component.state('value')).to.equal('badminton');
+ component
+ .find(Radio)
+ .at(1)
+ .simulate('change', { target: { value: 'soccer' } });
+ expect(component.state('value')).to.equal('soccer');
+ });
+
+ it('should call Radio props.onChange when selecting that radio button', () => {
+ const onChangeSwimming = sinon.spy();
+ const onChangeSoccer = sinon.spy();
+
+ const component = shallow(
+
+
+
+
+
+ );
+
+ component
+ .find(Radio)
+ .at(0)
+ .simulate('change', { target: { value: 'swimming' } });
+ expect(onChangeSwimming.calledOnce).to.equal(true);
+ expect(onChangeSoccer.called).to.equal(false);
+ expect(props.onChange.calledOnce).to.equal(true);
+ });
+
+ describe('getDerivedStateFromProps()', () => {
+ let component;
+
+ beforeEach(() => {
+ component = shallow(
+
+
+
+
+
+ );
+ });
+
+ it('should update selected value when `prop.value` changes', () => {
+ props.value = 'soccer';
+ component.setProps(props);
+ expect(component.state('value')).to.equal('soccer');
+ });
+
+ it('should NOT update selected value when other props attributes change', () => {
+ props.id = 'new-id';
+ props.name = 'some-other-name';
+ component.setProps(props);
+ expect(component.state('value')).to.equal('badminton');
+ });
+ });
+});
diff --git a/src/components/adslot-ui/index.js b/src/components/adslot-ui/index.js
index de528cda9..eb8841557 100644
--- a/src/components/adslot-ui/index.js
+++ b/src/components/adslot-ui/index.js
@@ -11,6 +11,8 @@ import ListPicker from 'adslot-ui/ListPicker';
import ListPickerPure from 'adslot-ui/ListPickerPure';
import PagedGrid from 'adslot-ui/PagedGrid';
import Panel from 'adslot-ui/Panel';
+import Radio from 'adslot-ui/Radio';
+import RadioGroup from 'adslot-ui/RadioGroup';
import Search from 'adslot-ui/Search';
import SearchBar from 'adslot-ui/SearchBar';
import SplitPane from 'adslot-ui/SplitPane';
@@ -42,6 +44,8 @@ export {
Nav,
PagedGrid,
Panel,
+ Radio,
+ RadioGroup,
Search,
SearchBar,
SplitPane,
diff --git a/src/index.js b/src/index.js
index f032877a3..45075ffd0 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,6 +1,4 @@
// Export the consumable components.
-import Radio from 'react-icheck/lib/Radio';
-import RadioGroup from 'react-icheck/lib/RadioGroup';
import Select from 'react-select';
import DatePicker from 'react-datepicker';
@@ -63,6 +61,8 @@ import {
Nav,
PagedGrid,
Panel,
+ Radio,
+ RadioGroup,
Search,
SearchBar,
SplitPane,