Unverified Commit 6ad42ee2 by Jae Bradley Committed by GitHub

Merge pull request #56 from edx/radio-button

Implement Radio Button Group
parents 7aaadaef 78d56290
...@@ -1010,6 +1010,116 @@ exports[`Storyshots Paragon Welcome 1`] = ` ...@@ -1010,6 +1010,116 @@ exports[`Storyshots Paragon Welcome 1`] = `
</div> </div>
`; `;
exports[`Storyshots RadioButtonGroup selected minimal usage 1`] = `
<div
aria-label="Radio Button Group"
onChange={[Function]}
role="radiogroup"
tabIndex={-1}
>
<div>
<input
aria-checked={false}
data-index={0}
defaultChecked={false}
name="rbg"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
type="radio"
value="jaebaebae"
/>
First Value
</div>
<div>
<input
aria-checked={true}
data-index={1}
defaultChecked={true}
name="rbg"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
type="radio"
value="value2"
/>
Second Value
</div>
<div>
<input
aria-checked={false}
data-index={2}
defaultChecked={false}
name="rbg"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
type="radio"
value="value3"
/>
Third Value
</div>
</div>
`;
exports[`Storyshots RadioButtonGroup unselected minimal usage 1`] = `
<div
aria-label="Radio Button Group"
onChange={[Function]}
role="radiogroup"
tabIndex={-1}
>
<div>
<input
aria-checked={false}
data-index={0}
defaultChecked={false}
name="rbg"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
type="radio"
value="jaebaebae"
/>
First Value
</div>
<div>
<input
aria-checked={false}
data-index={1}
defaultChecked={false}
name="rbg"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
type="radio"
value="value2"
/>
Second Value
</div>
<div>
<input
aria-checked={false}
data-index={2}
defaultChecked={false}
name="rbg"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
type="radio"
value="value3"
/>
Third Value
</div>
</div>
`;
exports[`Storyshots StatusAlert Non-dismissible alert 1`] = ` exports[`Storyshots StatusAlert Non-dismissible alert 1`] = `
<div <div
className="alert fade alert-danger show" className="alert fade alert-danger show"
......
{ {
"name": "@edx/paragon", "name": "@edx/paragon",
"version": "1.1.1", "version": "1.1.2",
"description": "Accessible, responsive UI component library based on Bootstrap.", "description": "Accessible, responsive UI component library based on Bootstrap.",
"main": "src/index.js", "main": "src/index.js",
"author": "arizzitano", "author": "arizzitano",
...@@ -27,6 +27,7 @@ ...@@ -27,6 +27,7 @@
}, },
"devDependencies": { "devDependencies": {
"@storybook/addon-actions": "^3.2.12", "@storybook/addon-actions": "^3.2.12",
"@storybook/addon-console": "^1.0.0",
"@storybook/addon-options": "^3.2.6", "@storybook/addon-options": "^3.2.6",
"@storybook/addon-storyshots": "^3.2.8", "@storybook/addon-storyshots": "^3.2.8",
"@storybook/react": "3.2.11", "@storybook/react": "3.2.11",
......
/* eslint-disable import/no-extraneous-dependencies, no-console */
import React from 'react';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { setConsoleOptions } from '@storybook/addon-console';
import RadioButtonGroup, { RadioButton } from './index';
setConsoleOptions({
panelExclude: ['warn', 'error'],
});
const onChange = (event) => {
console.log(`onChange fired for ${event.target.value}`);
const selectedIndex = parseInt(event.target.getAttribute('data-index'), 10);
console.log(`Selected index should be ${selectedIndex}`);
action('Radio Button Change');
};
const onClick = (event) => {
console.log(`onClick fired for ${event.target.value}`);
action('Radio Button Click');
};
const onFocus = (event) => {
console.log(`onFocus fired for ${event.target.value}`);
action('Radio Button Focus');
};
const onKeyDown = (event) => {
console.log(`onKeyDown fired for ${event.target.value} with key value: ${event.key}`);
action('Radio Button Key Press');
};
storiesOf('RadioButtonGroup', module)
.add('unselected minimal usage', () => (
<RadioButtonGroup
name={'rbg'}
label={'Radio Button Group'}
onBlur={action('Radio Button Blur')}
onChange={onChange}
onClick={onClick}
onFocus={onFocus}
onKeyDown={onKeyDown}
>
<RadioButton value={'jaebaebae'}>First Value</RadioButton>
<RadioButton value={'value2'}>Second Value</RadioButton>
<RadioButton value={'value3'}>Third Value</RadioButton>
</RadioButtonGroup>
))
.add('selected minimal usage', () => (
<RadioButtonGroup
name={'rbg'}
label={'Radio Button Group'}
onBlur={action('Radio Button Blur')}
onChange={onChange}
onClick={onClick}
onFocus={onFocus}
onKeyDown={onKeyDown}
selectedIndex={1}
>
<RadioButton value={'jaebaebae'}>First Value</RadioButton>
<RadioButton value={'value2'}>Second Value</RadioButton>
<RadioButton value={'value3'}>Third Value</RadioButton>
</RadioButtonGroup>
));
import React from 'react';
import { shallow, mount } from 'enzyme';
import RadioButtonGroup, { RadioButton } from './index';
describe('<RadioButton />', () => {
const text = 'text';
const index = 0;
const isChecked = false;
const name = 'name';
const onBlur = () => {};
const onClick = () => {};
const onFocus = () => {};
const onKeyDown = () => {};
const value = 'value';
const props = {
index,
isChecked,
name,
onBlur,
onClick,
onFocus,
onKeyDown,
value,
};
describe('correct rendering', () => {
it('renders RadioButton', () => {
const wrapper = shallow(<RadioButton {...props}>{text}</RadioButton>);
expect(wrapper.type()).toEqual('div');
expect(wrapper.find('input')).toHaveLength(1);
const radioButton = wrapper.find('input').at(0);
expect(radioButton.prop('type')).toEqual('radio');
expect(radioButton.prop('name')).toEqual(name);
expect(radioButton.prop('value')).toEqual(value);
expect(radioButton.prop('defaultChecked')).toEqual(isChecked);
expect(radioButton.prop('aria-checked')).toEqual(isChecked);
expect(radioButton.prop('data-index')).toEqual(index);
expect(wrapper.find('div').text()).toEqual(text);
});
});
describe('event handlers correctly triggered', () => {
let spy;
beforeEach(() => {
spy = jest.fn();
});
it('should fire onBlur', () => {
const wrapper = mount(<RadioButton {...props} onBlur={spy} />);
expect(spy).toHaveBeenCalledTimes(0);
wrapper.find('input').at(0).simulate('blur');
expect(spy).toHaveBeenCalledTimes(1);
});
it('should fire onClick', () => {
const wrapper = mount(<RadioButton {...props} onClick={spy} />);
expect(spy).toHaveBeenCalledTimes(0);
wrapper.find('input').at(0).simulate('click');
expect(spy).toHaveBeenCalledTimes(1);
});
it('should fire onFocus', () => {
const wrapper = mount(<RadioButton {...props} onFocus={spy} />);
expect(spy).toHaveBeenCalledTimes(0);
wrapper.find('input').at(0).simulate('focus');
expect(spy).toHaveBeenCalledTimes(1);
});
it('should fire onKeyDown', () => {
const wrapper = mount(<RadioButton {...props} onKeyDown={spy} />);
expect(spy).toHaveBeenCalledTimes(0);
wrapper.find('input').at(0).simulate('keydown');
expect(spy).toHaveBeenCalledTimes(1);
});
});
});
describe('<RadioButtonGroup />', () => {
const firstText = 'firstText';
const secondText = 'secondText';
const name = 'name';
const label = 'label';
const onBlur = () => {};
const onChange = () => {};
const onClick = () => {};
const onFocus = () => {};
const onKeyDown = () => {};
const firstValue = 'firstValue';
const secondValue = 'secondValue';
const props = {
name,
label,
onBlur,
onChange,
onClick,
onFocus,
onKeyDown,
};
describe('renders correctly', () => {
it('renders RadioButtonGroup', () => {
const radioButtonGroup = (
<RadioButtonGroup {...props}>
<RadioButton value={firstValue}>{firstText}</RadioButton>
<RadioButton value={secondValue}>{secondText}</RadioButton>
</RadioButtonGroup>
);
const wrapper = shallow(radioButtonGroup);
wrapper.find(RadioButton).forEach((button, index) => {
expect(button.prop('name')).toEqual(name);
expect(button.prop('isChecked')).toEqual(false);
expect(button.prop('onBlur')).toEqual(onBlur);
expect(button.prop('onClick')).toEqual(onClick);
expect(button.prop('onFocus')).toEqual(onFocus);
expect(button.prop('onKeyDown')).toEqual(onKeyDown);
expect(button.prop('index')).toEqual(index);
let value = firstValue;
if (index === 1) {
value = secondValue;
}
expect(button.prop('value')).toEqual(value);
});
const radioButtonGroupDiv = wrapper.find('div');
expect(radioButtonGroupDiv.prop('role')).toEqual('radiogroup');
expect(radioButtonGroupDiv.prop('aria-label')).toEqual(label);
expect(radioButtonGroupDiv.prop('tabIndex')).toEqual(-1);
});
});
describe('updates state when onChange event is fired', () => {
let spy;
const index = 7;
beforeEach(() => {
spy = jest.fn();
});
it('changes state when checked event and target has attribute', () => {
const event = {
target: {
checked: true,
hasAttribute: () => true,
getAttribute: () => index,
},
};
const radioButtonGroup = (
<RadioButtonGroup {...props} onChange={spy}>
<RadioButton value={firstValue}>{firstText}</RadioButton>
<RadioButton value={secondValue}>{secondText}</RadioButton>
</RadioButtonGroup>
);
const wrapper = mount(radioButtonGroup);
wrapper.simulate('change', event);
expect(spy).toHaveBeenCalledTimes(1);
expect(wrapper.state('selectedIndex')).toEqual(index);
});
it('does not change state if event target is not checked', () => {
const event = {
target: {
checked: false,
hasAttribute: () => true,
getAttribute: () => index,
},
};
const radioButtonGroup = (
<RadioButtonGroup {...props} onChange={spy}>
<RadioButton value={firstValue}>{firstText}</RadioButton>
<RadioButton value={secondValue}>{secondText}</RadioButton>
</RadioButtonGroup>
);
const wrapper = mount(radioButtonGroup);
wrapper.simulate('change', event);
expect(spy).toHaveBeenCalledTimes(1);
expect(wrapper.state('selectedIndex')).toEqual(undefined);
});
it('does not change state if event target is checked but data-attribute does not exist', () => {
const event = {
target: {
checked: false,
hasAttribute: () => false,
getAttribute: () => index,
},
};
const radioButtonGroup = (
<RadioButtonGroup {...props} onChange={spy}>
<RadioButton value={firstValue}>{firstText}</RadioButton>
<RadioButton value={secondValue}>{secondText}</RadioButton>
</RadioButtonGroup>
);
const wrapper = mount(radioButtonGroup);
wrapper.simulate('change', event);
expect(spy).toHaveBeenCalledTimes(1);
expect(wrapper.state('selectedIndex')).toEqual(undefined);
});
});
});
import React from 'react';
import PropTypes from 'prop-types';
function RadioButton(props) {
const {
children,
index,
isChecked,
name,
onBlur,
onClick,
onFocus,
onKeyDown,
value,
...other
} = props;
return (
<div>
<input
type={'radio'}
name={name}
aria-checked={isChecked}
defaultChecked={isChecked}
value={value}
data-index={index}
onBlur={event => onBlur(event)}
onClick={event => onClick(event)}
onFocus={event => onFocus(event)}
onKeyDown={event => onKeyDown(event)}
{...other}
/>{children}
</div>
);
}
class RadioButtonGroup extends React.Component {
constructor(props) {
super();
// Bind the method to the component context
this.renderChildren = this.renderChildren.bind(this);
this.state = {
selectedIndex: props.selectedIndex,
};
}
onChange(event) {
if (event.target.checked && event.target.hasAttribute('data-index')) {
this.setState({
selectedIndex: parseInt(event.target.getAttribute('data-index'), 10),
});
}
this.props.onChange(event);
}
renderChildren() {
return React.Children.map((this.props.children), (child, index) =>
React.cloneElement(child, {
name: this.props.name,
value: child.props.value,
isChecked: index === this.state.selectedIndex,
onBlur: this.props.onBlur,
onClick: this.props.onClick,
onFocus: this.props.onFocus,
onKeyDown: this.props.onKeyDown,
index,
}),
);
}
render() {
const {
children,
label,
name,
onBlur,
onChange,
onClick,
onFocus,
onKeyDown,
selectedIndex,
...other
} = this.props;
return (
<div
role={'radiogroup'}
aria-label={label}
onChange={event => this.onChange(event)}
tabIndex={-1}
{...other}
>
{this.renderChildren()}
</div>
);
}
}
RadioButton.defaultProps = {
children: undefined,
index: undefined,
isChecked: false,
name: undefined,
onBlur: () => {},
onClick: () => {},
onFocus: () => {},
onKeyDown: () => {},
};
RadioButton.propTypes = {
children: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
PropTypes.bool,
]),
index: PropTypes.number,
isChecked: PropTypes.bool,
name: PropTypes.string,
onBlur: PropTypes.func,
onClick: PropTypes.func,
onFocus: PropTypes.func,
onKeyDown: PropTypes.func,
value: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
PropTypes.bool,
]).isRequired,
};
RadioButtonGroup.defaultProps = {
onBlur: () => {},
onChange: () => {},
onClick: () => {},
onFocus: () => {},
onKeyDown: () => {},
selectedIndex: undefined,
};
RadioButtonGroup.propTypes = {
children: PropTypes.arrayOf(RadioButton).isRequired,
label: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
onBlur: PropTypes.func,
onChange: PropTypes.func,
onClick: PropTypes.func,
onFocus: PropTypes.func,
onKeyDown: PropTypes.func,
selectedIndex: PropTypes.number,
};
export { RadioButtonGroup as default, RadioButton };
...@@ -4,6 +4,7 @@ import Dropdown from './Dropdown'; ...@@ -4,6 +4,7 @@ import Dropdown from './Dropdown';
import InputSelect from './InputSelect'; import InputSelect from './InputSelect';
import InputText from './InputText'; import InputText from './InputText';
import Tabs from './Tabs'; import Tabs from './Tabs';
import RadioButtonGroup, { RadioButton } from './RadioButtonGroup';
export { export {
Button, Button,
...@@ -12,4 +13,6 @@ export { ...@@ -12,4 +13,6 @@ export {
InputSelect, InputSelect,
InputText, InputText,
Tabs, Tabs,
RadioButtonGroup,
RadioButton,
}; };
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment