There are no notes for this item.
Prop | Required? | Type | Default | Description |
---|---|---|---|---|
additionalFieldsetClass | No | string | additional fieldset classes | |
additionalLegendClass | No | string | additional legend classes | |
errorMessage | No | string | radio button group error message | |
label | Yes | union | radio button group field label | |
name | No | string | name attribute | |
id | No | string | ||
toolTipText | No | string | help text for user | |
tabIndex | No | number | keyboard tab order for radio button group | |
options | Yes | [union] | array of options to populate group- each item is a string or an object representing an Expanding Group | |
value | Yes | { value: string, dirty: bool } |
value object for selected field value: string value that matches radio button value dirty: indicates if form is dirty; should be true after any user input |
|
onValueChange | Yes | func | handler for the value change | |
required | No | bool | toggles required field indicator |
<div id="reactMount" data-tpl="errorableradiobuttons">
<fieldset class="fieldset-input usa-input-error">
<legend class="legend-label usa-input-error-label">Errorable Radio Buttons</legend><span class="usa-input-error-message" role="alert" id="defaultId-error-message"><span class="sr-only">Error</span> Radio Button errorMessage</span>
<div class="form-radio-buttons"><input type="radio" autocomplete="false" id="defaultId-0" name="defaultName" value="option 1" /><label name="defaultName-0-label" for="defaultId-0">option 1</label></div>
<div class="form-radio-buttons"><input type="radio" autocomplete="false" checked="" id="defaultId-1" name="defaultName" value="option 2" /><label name="defaultName-1-label" for="defaultId-1">option 2</label></div>
<div class="form-expanding-group">
<div class="form-radio-buttons"><input type="radio" autocomplete="false" id="defaultId-2" name="defaultName" value="expanding option 3" /><label name="defaultName-2-label" for="defaultId-2">option 3 label</label></div><span></span></div>
</fieldset>
</div>
<script>
window.currentProps = {
"package": {
"name": "department-of-veteran-affairs/jean-pants",
"version": "0.1.0"
},
"assetPath": "/design-system/",
"isProduction": true,
"componentSourcePath": "./ErrorableRadioButtons.jsx",
"id": "defaultId",
"name": "defaultName",
"errorMessage": "Radio Button errorMessage",
"label": "Errorable Radio Buttons",
"options": ["option 1", "option 2", {
"label": "option 3 label",
"value": "expanding option 3",
"additional": "expanded option 3"
}],
"value": {
"value": "option 2",
"dirty": null
},
"required": null
}
</script>
import React from 'react';
import ErrorableRadioButtons from './ErrorableRadioButtons';
class StateWrapper extends React.Component {
constructor(props) {
super(props);
this.state = props;
}
render() {
return (
<ErrorableRadioButtons
onValueChange={({value}) => {
this.setState({value:{value, dirty: true}});
}}
{...this.state}
/>
);
}
}
export default function ErrorableRadioButtonsExample(props) {
return (
<StateWrapper
{...props}
>
<ErrorableRadioButtons
onValueChange={()=>{}}
{...props}
/>
</StateWrapper>
);
}
package:
name: department-of-veteran-affairs/jean-pants
version: 0.1.0
assetPath: /design-system/
isProduction: true
componentSourcePath: ./ErrorableRadioButtons.jsx
id: defaultId
name: defaultName
errorMessage: Radio Button errorMessage
label: Errorable Radio Buttons
options:
- option 1
- option 2
- label: option 3 label
value: expanding option 3
additional: expanded option 3
value:
value: option 2
dirty: null
required: null
import PropTypes from 'prop-types';
import React from 'react';
import _ from 'lodash';
import classNames from 'classnames';
import ToolTip from '../../../Tooltip/Tooltip';
import ExpandingGroup from '../ExpandingGroup/ExpandingGroup';
import { makeField } from '../../../../helpers/fields';
/**
* A radio button group with a label.
*
* Validation has the following props.
* `additionalFieldsetClass` - String for any additional fieldset classes.
* `additionalLegendClass` - String for any additional legend classes.
* `errorMessage' - String Error message for the radio button group
* `label` - String for the group field label.
* `name` - String for the name attribute.
* `toolTipText` - String with help text for user.
* `tabIndex` - Number for keyboard tab order.
* `options` - Array of options to populate group.
* `required` - is this field required.
* `value` - string. Value of the select field.
* `onValueChange` - a function with this prototype: (newValue)
*/
class ErrorableRadioButtons extends React.Component {
constructor() {
super();
this.handleChange = this.handleChange.bind(this);
this.getMatchingSubSection = this.getMatchingSubSection.bind(this);
}
componentWillMount() {
this.inputId = this.props.id || _.uniqueId('errorable-radio-buttons-');
}
getMatchingSubSection(checked, optionValue) {
if (checked && this.props.children) {
const children = _.isArray(this.props.children)
? this.props.children
: [this.props.children];
const subsections = children.filter(
child => child.props.showIfValueChosen === optionValue
);
return subsections.length > 0 ? subsections[0] : null;
}
return null;
}
handleChange(domEvent) {
this.props.onValueChange(makeField(domEvent.target.value, true));
}
render() {
// TODO: extract error logic into a utility function
// Calculate error state.
let errorSpan = '';
let errorSpanId = undefined;
if (this.props.errorMessage) {
errorSpanId = `${this.inputId}-error-message`;
errorSpan = (
<span className="usa-input-error-message" role="alert" id={errorSpanId}>
<span className="sr-only">Error</span> {this.props.errorMessage}
</span>
);
}
// Addes ToolTip if text is provided.
let toolTip;
if (this.props.toolTipText) {
toolTip = (
<ToolTip
tabIndex={this.props.tabIndex}
toolTipText={this.props.toolTipText}/>
);
}
// Calculate required.
let requiredSpan = undefined;
if (this.props.required) {
requiredSpan = <span className="form-required-span">*</span>;
}
const options = _.isArray(this.props.options) ? this.props.options : [];
const storedValue = this.props.value.value;
const optionElements = options.map((obj, index) => {
let optionLabel;
let optionValue;
let optionAdditional;
if (_.isString(obj)) {
optionLabel = obj;
optionValue = obj;
} else {
optionLabel = obj.label;
optionValue = obj.value;
if (obj.additional) {
optionAdditional = <div>{obj.additional}</div>;
}
}
const checked = optionValue === storedValue ? 'checked=true' : '';
const matchingSubSection = this.getMatchingSubSection(
optionValue === storedValue,
optionValue
);
const radioButton = (
<div
key={optionAdditional ? undefined : index}
className="form-radio-buttons">
<input
autoComplete="false"
checked={checked}
id={`${this.inputId}-${index}`}
name={this.props.name}
type="radio"
value={optionValue}
onChange={this.handleChange}/>
<label
name={`${this.props.name}-${index}-label`}
htmlFor={`${this.inputId}-${index}`}>
{optionLabel}
</label>
{matchingSubSection}
</div>
);
let output = radioButton;
// Return an expanding group for buttons with additional content
if (optionAdditional) {
output = (
<ExpandingGroup
additionalClass="form-expanding-group-active-radio"
open={!!checked}
key={index}>
{radioButton}
<div>{optionAdditional}</div>
</ExpandingGroup>
);
}
return output;
});
const fieldsetClass = classNames('fieldset-input', {
'usa-input-error': this.props.errorMessage,
[this.props.additionalFieldsetClass]: this.props.additionalFieldsetClass
});
const legendClass = classNames('legend-label', {
'usa-input-error-label': this.props.errorMessage,
[this.props.additionalLegendClass]: this.props.additionalLegendClass
});
return (
<fieldset className={fieldsetClass}>
<legend className={legendClass}>
{this.props.label}
{requiredSpan}
</legend>
{errorSpan}
{optionElements}
{toolTip}
</fieldset>
);
}
}
ErrorableRadioButtons.propTypes = {
/**
* additional fieldset classes
*/
additionalFieldsetClass: PropTypes.string,
/**
* additional legend classes
*/
additionalLegendClass: PropTypes.string,
/**
* radio button group error message
*/
errorMessage: PropTypes.string,
/**
* radio button group field label
*/
label: PropTypes.oneOfType([PropTypes.string, PropTypes.element]).isRequired,
/**
* name attribute
*/
name: PropTypes.string,
id: PropTypes.string,
/**
* help text for user
*/
toolTipText: PropTypes.string,
/**
* keyboard tab order for radio button group
*/
tabIndex: PropTypes.number,
/**
* array of options to populate group- each item is a string or an object representing an Expanding Group
*
*/
options: PropTypes.arrayOf(
PropTypes.oneOfType([
PropTypes.string,
PropTypes.shape({
label: PropTypes.oneOfType([PropTypes.string, PropTypes.element])
.isRequired,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.bool])
.isRequired,
additional: PropTypes.oneOfType([PropTypes.string, PropTypes.element])
})
])
).isRequired,
/**
* value object for selected field <br/>
* value: string value that matches radio button value </br>
* dirty: indicates if form is dirty; should be true after any user input
*/
value: PropTypes.shape({
/**
* value of the select field.
*/
value: PropTypes.string,
dirty: PropTypes.bool
}).isRequired,
/**
* handler for the value change
*/
onValueChange: PropTypes.func.isRequired,
/**
* toggles required field indicator
*/
required: PropTypes.bool
};
export default ErrorableRadioButtons;
import React from 'react';
import { expect } from 'chai';
import {
shallow,
mount
} from 'enzyme';
import { axeCheck } from '../../../../../lib/testing/helpers';
import ErrorableRadioButtons from './ErrorableRadioButtons.jsx';
import { makeField } from '../../../../helpers/fields.js';
describe('<ErrorableRadioButtons>', () => {
const nonExpandingOptions = ['yes', 'no'];
const allExpandingOptions = [{ value: 'yes', label: 'Yes', additional: 'Yes addtional' }, { value: 'no', label: 'No', additional: 'No additional' }];
const someExpandingOptions = ['yes', { value: 'no', label: 'No', additional: 'No additional' }, 'maybe'];
it('calls onValueChange with value and dirty state', () => {
let valueChanged;
// shallowly render component with callback that alters valueChanged with passed argument
const wrapper = mount(<ErrorableRadioButtons label="test" options={nonExpandingOptions} value={makeField('test')} onValueChange={(value) => {valueChanged = value;}}/>);
// simulate change event on first input
wrapper.find('input').first().simulate('change');
// verify that change event value matches first value in options passed to component
expect(valueChanged.value).to.eql(nonExpandingOptions[0]);
expect(valueChanged.dirty).to.eql(true);
});
it('renders label htmlFor attribute with correct input id attribute value', () => {
const wrapper = shallow(<ErrorableRadioButtons label="test" options={nonExpandingOptions} value={makeField('test')} onValueChange={(value) => value}/>);
// gather input id and label for attributes from render component
const inputIds = wrapper.find('input').map((inputId) => inputId.prop('id'));
const labelFors = wrapper.find('label').map((labelFor) => labelFor.prop('htmlFor'));
// assert each input id attribute value matches respective label for attribute value
inputIds.forEach((inputId, index) => expect(inputId).to.eql(labelFors[index]));
});
it('renders a legend tag with the label attribute', () => {
const labelValue = 'test';
const wrapper = shallow(<ErrorableRadioButtons label={labelValue} options={nonExpandingOptions} value={makeField('test')} onValueChange={(value) => value}/>);
// assert that legend element was rendered with label value as its text
const legendText = wrapper.find('legend').text();
expect(legendText).to.eql(labelValue);
});
it('passes aXe check when only non-expanding options are rendered', () => {
const check = axeCheck(<ErrorableRadioButtons label="test" options={nonExpandingOptions} value={makeField('test')} onValueChange={(value) => value}/>);
return check;
});
it('passes aXe check when only expanding options are rendered', () => {
const check = axeCheck(<ErrorableRadioButtons label="test" options={allExpandingOptions} value={makeField('test')} onValueChange={(value) => value}/>);
return check;
});
it('passes aXe check when non-expanding and expanding options rendered', () => {
const check = axeCheck(<ErrorableRadioButtons label="test" options={someExpandingOptions} value={makeField('test')} onValueChange={(value) => value}/>);
return check;
});
});