Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[added] RadioButton and RadioGroup #962

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/examples/InputTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const inputTypeInstance = (
<Input type="password" label="Password" />
<Input type="file" label="File" help="[Optional] Block level help text" />
<Input type="checkbox" label="Checkbox" checked readOnly />
<Input type="radio" label="Radio" checked readOnly />
<FormControls.RadioButton label="Radio" checked readOnly />
<Input type="select" label="Select" placeholder="select">
<option value="select">select</option>
<option value="other">...</option>
Expand Down
39 changes: 39 additions & 0 deletions docs/examples/RadioButtonGroup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
const SimpleRadioGroup = React.createClass({
getInitialState() {
return {message: undefined};

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think return {} shoud be better

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

({message: undefined}).message=== ({}).message returns true

},

handleChange(newValue) {
this.setState({message: newValue});
},

render() {
const message = this.state.message ?
<FormControls.Static>{this.state.message}</FormControls.Static> :
null;

const handleChange = this.handleChange;

return (
<form>
{message}
<FormControls.RadioGroup name="greeting" inline legend="Greetings" srOnly>
<FormControls.RadioButton value="Hi!">Hi!</FormControls.RadioButton>
<FormControls.RadioButton value="Hello!">Hello!</FormControls.RadioButton>
</FormControls.RadioGroup>
<FormControls.RadioGroup name="farewell" onChange={handleChange}>
<FormControls.RadioButton label="Bye!" value="Bye!" />
<FormControls.RadioButton label="Goodbye!" value="Goodbye!" />
</FormControls.RadioGroup>
<FormControls.RadioGroup name="thing" defaultValue="2">
<FormControls.RadioButton label="1" value="1" disabled />
<FormControls.RadioButton label="2" value="2" disabled />
<FormControls.RadioButton label="3" value="3" disabled />
</FormControls.RadioGroup>
<FormControls.RadioButton value="5" />
</form>
);
}
});

React.render(<SimpleRadioGroup />, mountNode);
8 changes: 7 additions & 1 deletion docs/src/ComponentsPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -836,7 +836,13 @@ const ComponentsPage = React.createClass({
<h3><Anchor id="button-input-types">Button Input Types</Anchor></h3>
<p>Form buttons are encapsulated by <code>ButtonInput</code>. Pass in <code>type="reset"</code> or <code>type="submit"</code> to suit your needs. Styling is the same as <code>Button</code>.</p>
<ReactPlayground codeText={Samples.ButtonInput} />

<h3><Anchor id="radio-group">Radio Group</Anchor></h3>
<p>Radio buttons can be created with <code>FormControls.RadioButton</code>. <code>FormControls.RadioGroup</code> will wrap your group of radio buttons in a fieldset.</p>
<ReactPlayground codeText={Samples.RadioButtonGroup} />
<h4><Anchor id="radio-group-props"><code>FormControls.RadioGroup</code> Props</Anchor></h4>
<PropTable component="RadioGroup" />
<h4><Anchor id="radio-button-props"><code>FormControls.RadioButton</code> Props</Anchor></h4>
<PropTable component="RadioButton" />
<h3><Anchor id="input-addons">Add-ons</Anchor></h3>
<p>Use <code>addonBefore</code> and <code>addonAfter</code> for normal addons, <code>buttonBefore</code> and <code>buttonAfter</code> for button addons.
Exotic configurations may require some css on your side.</p>
Expand Down
1 change: 1 addition & 0 deletions docs/src/Samples.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ export default {
InputTypes: require('fs').readFileSync(__dirname + '/../examples/InputTypes.js', 'utf8'),
StaticText: require('fs').readFileSync(__dirname + '/../examples/StaticText.js', 'utf8'),
ButtonInput: require('fs').readFileSync(__dirname + '/../examples/ButtonInput.js', 'utf8'),
RadioButtonGroup: require('fs').readFileSync(__dirname + '/../examples/RadioButtonGroup.js', 'utf8'),
InputAddons: require('fs').readFileSync(__dirname + '/../examples/InputAddons.js', 'utf8'),
InputSizes: require('fs').readFileSync(__dirname + '/../examples/InputSizes.js', 'utf8'),
InputValidation: require('fs').readFileSync(__dirname + '/../examples/InputValidation.js', 'utf8'),
Expand Down
92 changes: 92 additions & 0 deletions src/FormControls/RadioButton.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import React from 'react';
import classNames from 'classnames';
import { all, singlePropFrom } from '../utils/CustomPropTypes';

const singleNodeFromChildAndLabel = all([
singlePropFrom(['children', 'label']),
React.PropTypes.node
]);

const propTypes = {
/**
* Wraps the radio button in a label along with the provided content. Do not use with the label prop
*/
children: singleNodeFromChildAndLabel,
/**
* Disables the radio button
*/
disabled: React.PropTypes.bool,
/**
* Applies the .radio-inline class to the wrapping label
*/
inline: React.PropTypes.bool,
/**
* Wraps the radio button in a label along with the provided content. Do not use with children
*/
label: singleNodeFromChildAndLabel,
/**
* Apply a css class directly to the wrapping label tag (if label is provided)
*/
labelClassName: React.PropTypes.string,
/**
* Applies a name to associate the radio button with a group.
*/
name: React.PropTypes.string,
/**
* Calls this when the checked state changes
*/
onChange: React.PropTypes.func,
/**
* Applies a value to this radio button
*/
value: React.PropTypes.any
};

export default class RadioButton extends React.Component {
getInputDOMNode() {
return React.findDOMNode(this.refs.input);
}

getValue() {
return this.props.value;
}

getChecked() {
return this.getInputDOMNode().checked;
}

renderWrapper(label, inline) {
return inline ? label : <div className="radio">{label}</div>;
}

renderLabel(input) {
if (!this.props.label && !this.props.children) {
return input;
}

const {children, id, label, inline, ...other} = this.props;
const innerContent = children ? children : label;

const classes = {
'radio-inline': inline
};

const wrapped = (
<label className={classNames(classes, this.props.labelClassName)} htmlFor={id}>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the past I have had trouble adding the class radio directly to the label element. tbs docs add it to a surrounding div when used in a FormGroup in a horizontal form, otherwise something is off about the margins. We should confirm that that isn't a problem here. According to our docs we follow the same behavior for checkbox

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is where my headache's been so far. For regular radio buttons the styling works when wrapped in the div as you said, but for inline it must be on the label without wrapping.

{input}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the label should probably also add htmlFor if there is an id prop, for better accessibility. We might also want to add id as a requiredForA11y prop to encourage good behavior. I wouldn't assume the prop is there tho to avoid breaking compatibility

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point! I initially didn't implement it since the wrapping automatically associated the radio button with the label and gave me the green light through the WAVE tool. Having the id would certainly be a good enhancement, though I don't think I'll be making it a required prop.

{innerContent}
</label>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

id and label vars from 26 line

const {id, label} = this.props;

are not used anywhere else
why don't just ?

<label className={classNames(classes)} htmlFor={this.props.id}>
  {input}
  {this.props.label}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks cleaner to me this way, and babel will transform it to this.props.id anyway.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the clarification 🍒
no problem at all with it. it is just a matter of personal taste 😉

);

return this.renderWrapper(wrapped, inline);
}

render() {
const {children, inline, label, ...other} = this.props;
return this.renderLabel(
<input type="radio" {...other} ref="input" />
);
}
}

RadioButton.propTypes = propTypes;
87 changes: 87 additions & 0 deletions src/FormControls/RadioGroup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import React from 'react';
import classNames from 'classnames';
import ValidComponentChildren from '../utils/ValidComponentChildren';

function noop() { }

export default class RadioGroup extends React.Component {
constructor(props) {
super(props);
this.state = {value: this.props.defaultValue};
this.handleChange = this.handleChange.bind(this);
}

getValue() {
return this.state.value;
}

renderLegend() {
const {legend, srOnly} = this.props;
const classes = {
'sr-only': srOnly
};

return legend ?
<legend className={classNames(classes)}>{legend}</legend> :
null;
}

handleChange(evt) {
if (!evt.target) {
return;
}

const {value} = evt.target;
this.setState({value});
this.props.onChange(value);
}

renderChildren() {
const {name, inline} = this.props;
return ValidComponentChildren.map(this.props.children, child => {
const {value} = child.props;
const checked = this.state.value ? this.state.value === value : false;
return React.cloneElement(child, {name, inline, checked, onChange: this.handleChange});
});
}

render() {
return (
<fieldset ref="radioGroup" className={classNames('form-group', this.props.className)}>
{this.renderLegend()}
{this.renderChildren()}
</fieldset>
);
}
}

RadioGroup.propTypes = {
/**
* The default value of the radio group. The RadioButton in this group with the provided value will be checked by default.
*/
defaultValue: React.PropTypes.any,
/**
* Formats the radio group with the class .radio-inline
*/
inline: React.PropTypes.bool,
/**
* Populates a legend element at the top of the fieldset
*/
legend: React.PropTypes.node,
/**
* Applies the provided name prop to all the given children.
*/
name: React.PropTypes.string.isRequired,
/**
* This is called when the selected value changes, with the new value as a parameter
*/
onChange: React.PropTypes.func,
/**
* Applies the legend, if present, with the .sr-only class
*/
srOnly: React.PropTypes.bool
};

RadioGroup.defaultProps = {
onChange: noop
};
10 changes: 9 additions & 1 deletion src/FormControls/index.js
Original file line number Diff line number Diff line change
@@ -1 +1,9 @@
export Static from './Static';
import RadioButton from './RadioButton';
import RadioGroup from './RadioGroup';
import Static from './Static';

export default {
Static,
RadioGroup,
RadioButton
};
3 changes: 3 additions & 0 deletions src/Input.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ class Input extends InputBase {
if (this.props.type === 'static') {
deprecationWarning('Input type=static', 'StaticText');
return <FormControls.Static {...this.props} />;
} else if (this.props.type === 'radio') {
deprecationWarning('Input type=radio', 'FormControls.RadioButton and FormControls.RadioGroup');
return <FormControls.RadioButton {...this.props} />;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docs are stull using this form so two things:

  1. maybe update the docs to not use Input?
  2. this also needs to wrap the RadioButton in a FormGroup to be consistent with the old behavior

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Docs will be updated.
  2. That my be so, but using FormGroup on RadioButton is causing all sorts of headache, mainly with inline styles.

}

return super.render();
Expand Down
15 changes: 5 additions & 10 deletions src/utils/childrenValueInputValidation.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
import React from 'react';
import { singlePropFrom } from './CustomPropTypes';
import { all, singlePropFrom } from './CustomPropTypes';

const propList = ['children', 'value'];

export default function valueValidation(props, propName, componentName) {
let error = singlePropFrom(propList)(props, propName, componentName);

if (!error) {
error = React.PropTypes.node(props, propName, componentName);
}

return error;
}
export default all([
singlePropFrom(propList),
React.PropTypes.node
]);
1 change: 1 addition & 0 deletions test/.eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"globals": {
"assert": true,
"expect": true,
"should": true,
"sinon": true
},
"plugins": [
Expand Down
76 changes: 76 additions & 0 deletions test/FormControls/RadioButtonSpec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import React from 'react';
import ReactTestUtils from 'react/lib/ReactTestUtils';
import RadioButton from '../../src/FormControls/RadioButton';

describe('FormControls.RadioButton', function () {
it('does not apply htmlFor to the label if no id is provided', function () {
const instance = ReactTestUtils.renderIntoDocument(
<RadioButton label="test" value="test" />
);

const label = ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'label');
should.not.exist(label.props.htmlFor);
});

it('does not apply htmlFor to the label if no id is provided', function () {
const testId = 'thing';
const instance = ReactTestUtils.renderIntoDocument(
<RadioButton id={testId} label="test" value="test" />
);

const label = ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'label');
label.props.htmlFor.should.eql(testId);
});

it('does not render a label if no label prop is provided', function () {
const instance = ReactTestUtils.renderIntoDocument(
<RadioButton value="test" />
);

React.findDOMNode(instance).tagName.toLowerCase().should.eql('input');
});

it('renders a label if label prop is provided', function () {
const instance = ReactTestUtils.renderIntoDocument(
<RadioButton label="thing" value="test" />
);

ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'label');
});

it('renders a div around the label if not inline', () => {
const instance = ReactTestUtils.renderIntoDocument(
<RadioButton label="thing" value="test" />
);

React.findDOMNode(instance).tagName.toLowerCase().should.equal('div');
});

it('should not wrap the label with a div if inline', () => {
const instance = ReactTestUtils.renderIntoDocument(
<RadioButton inline label="thing" value="test" />
);

React.findDOMNode(instance).tagName.toLowerCase().should.equal('label');
});

it('should render the label prop', function () {
const testText = 'thing';
const instance = ReactTestUtils.renderIntoDocument(
<RadioButton label={testText} value="test" />
);

React.findDOMNode(instance).textContent.should.equal(testText);
});

it('should render the children', () => {
const testText = 'thing';
const instance = ReactTestUtils.renderIntoDocument(
<RadioButton value="test">
{testText}
</RadioButton>
);

React.findDOMNode(instance).textContent.should.equal(testText);
});
});
Loading