There are no notes for this item.
Prop | Required? | Type | Default | Description |
---|---|---|---|---|
errorMessage | No | string | ||
name | No | string | ||
autocomplete | No | string | ||
label | Yes | union | ||
options | Yes | [union] | ||
required | No | bool | ||
includeBlankOption | No | bool | true | |
emptyDescription | No | string | ||
value | Yes | { value: string, dirty: bool } |
||
onValueChange | Yes | func | ||
additionalClass | No | string |
<div id="reactMount" data-tpl="errorableselect">
<div class="usa-input-error"><label class="usa-input-error-label" for="errorable-select-40"><span class="form-required-span">*</span></label><span class="usa-input-error-message" id="errorable-select-40-error-message" role="alert">This is the error message</span><select aria-describedby="errorable-select-40-error-message"
id="errorable-select-40" name="Attribute name"><option value=""></option><option value="first option">first option</option><option value="second option">second option</option><option value="third option">third option</option></select></div>
</div>
<script>
window.currentProps = {
"package": {
"name": "department-of-veteran-affairs/jean-pants",
"version": "0.1.0"
},
"assetPath": "/design-system/",
"isProduction": true,
"componentSourcePath": "./ErrorableSelect.jsx",
"errorMessage": "This is the error message",
"name": "Attribute name",
"options": ["first option", "second option", "third option"],
"required": true,
"value": {
"value": "Value",
"dirty": true
}
}
</script>
import React from 'react';
import ErrorableSelect from './ErrorableSelect';
export default function ErrorableSelectExample(props) {
return (
<ErrorableSelect
errorMessage={props.errorMessage}
name={props.name}
autocomplete={props.autocomplete}
label={props.label}
options={props.options}
required={props.required}
includeBlankOption={props.includeBlankOption}
value={props.value}
onValueChange={props.onValueChange}
additionalClass={props.additionalClass}
emptyDescription={props.emptyDescription}
/>)
}
package:
name: department-of-veteran-affairs/jean-pants
version: 0.1.0
assetPath: /design-system/
isProduction: true
componentSourcePath: ./ErrorableSelect.jsx
errorMessage: This is the error message
name: Attribute name
options:
- first option
- second option
- third option
required: true
value:
value: Value
dirty: true
import PropTypes from 'prop-types';
import React from 'react';
import _ from 'lodash';
import ToolTip from '../../../Tooltip/Tooltip';
import { makeField } from '../../../../helpers/fields';
/**
* A form select with a label that can display error messages.
*/
class ErrorableSelect extends React.Component {
constructor() {
super();
this.handleChange = this.handleChange.bind(this);
}
componentWillMount() {
this.selectId = _.uniqueId('errorable-select-');
}
handleChange(domEvent) {
this.props.onValueChange(makeField(domEvent.target.value, true));
}
render() {
const selectedValue = this.props.value.value;
// Calculate error state.
let errorSpan = '';
let errorSpanId = undefined;
if (this.props.errorMessage) {
errorSpanId = `${this.selectId}-error-message`;
errorSpan = (
<span
className="usa-input-error-message"
id={`${errorSpanId}`}
role="alert">
{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>;
}
// Calculate options for select
let reactKey = 0;
// TODO(awong): Remove this hack to handle options prop and use invariants instead.
const options = _.isArray(this.props.options) ? this.props.options : [];
const optionElements = options.map(obj => {
let label;
let value;
if (_.isString(obj)) {
label = obj;
value = obj;
} else {
label = obj.label;
value = obj.value;
}
return (
<option key={++reactKey} value={value}>
{label}
</option>
);
});
return (
<div className={this.props.errorMessage ? 'usa-input-error' : undefined}>
<label
className={
this.props.errorMessage !== undefined
? 'usa-input-error-label'
: this.props.labelClass
}
htmlFor={this.selectId}>
{this.props.label}
{requiredSpan}
</label>
{errorSpan}
<select
className={this.props.selectClass || this.props.additionalClass}
aria-describedby={errorSpanId}
id={this.selectId}
name={this.props.name}
autoComplete={this.props.autocomplete}
value={selectedValue}
onChange={this.handleChange}>
{this.props.includeBlankOption && (
<option value="">{this.props.emptyDescription}</option>
)}
{optionElements}
</select>
{toolTip}
</div>
);
}
}
ErrorableSelect.propTypes = {
// Error string to display in the component.
// When defined, indicates select has a validation error.
errorMessage: PropTypes.string,
// Select name attribute.
name: PropTypes.string,
// Select autocomplete attribute.
autocomplete: PropTypes.string,
// Select field label.
label: PropTypes.oneOfType([PropTypes.string, PropTypes.element]).isRequired,
// Array of options to populate select.
options: PropTypes.arrayOf(
PropTypes.oneOfType([
PropTypes.string,
PropTypes.shape({
label: PropTypes.string,
value: PropTypes.number
}),
PropTypes.shape({
label: PropTypes.string,
value: PropTypes.string
})
])
).isRequired,
// Render marker indicating field is required.
required: PropTypes.bool,
// is there an empty selectable thing
includeBlankOption: PropTypes.bool,
// Description that shows up for the blank option, when includeBlankOption is true
emptyDescription: PropTypes.string,
/* `value` - object containing:
* - `value`: Value of the select field.
* - `dirty`: boolean. Whether a field has been touched by the user.
*/
value: PropTypes.shape({
value: PropTypes.string,
dirty: PropTypes.bool
}).isRequired,
// a function with this prototype: (newValue)
onValueChange: PropTypes.func.isRequired,
// Additional css class that is added to the select element.
additionalClass: PropTypes.string
};
ErrorableSelect.defaultProps = {
includeBlankOption: true
};
export default ErrorableSelect;
import React from 'react';
import { mount, shallow } from 'enzyme';
import chaiAsPromised from 'chai-as-promised';
import chai, { expect } from 'chai';
import { axeCheck } from '../../../../../lib/testing/helpers';
import ErrorableSelect from './ErrorableSelect.jsx';
import { makeField } from '../../../../helpers/fields.js';
chai.use(chaiAsPromised);
describe('<ErrorableSelect>', () => {
const testValue = makeField('');
const options = [{ value: 1, label: 'first' }, { value: 2, label: 'second' }];
it('calls onValueChange with input value', () => {
let valueChanged;
// render component with callback that alters valueChanged with passed argument
const wrapper = mount(<ErrorableSelect
label="my label"
options={options}
value={testValue}
onValueChange={(value) => { valueChanged = value; }}/>);
wrapper.find('select').first().simulate('change', { target: { value: 'hello' } });
expect(valueChanged.value).to.eql('hello');
});
it('no error styles when errorMessage undefined', () => {
const tree = shallow(<ErrorableSelect label="my label" options={options} value={testValue} onValueChange={(_update) => {}}/>);
// No error classes.
expect(tree.find('.usa-input-error')).to.have.lengthOf(0);
expect(tree.find('.usa-input-error-label')).to.have.lengthOf(0);
expect(tree.find('.usa-input-error-message')).to.have.lengthOf(0);
// Ensure no unnecessary class names on label w/o error.
const labels = tree.find('label');
expect(labels).to.have.lengthOf(1);
expect(labels.hasClass('')).to.be.true;
// No error means no aria-describedby to not confuse screen readers.
const selects = tree.find('select');
expect(selects).to.have.lengthOf(1);
expect(selects.find('aria-describedby')).to.have.lengthOf(0);
});
it('should pass aXe check when errorMessage is undefined', () => {
return axeCheck(<ErrorableSelect label="my label" options={options} value={testValue} onValueChange={(_update) => {}}/>);
});
it('has error styles when errorMessage is set', () => {
const tree = shallow(<ErrorableSelect label="my label" options={options} errorMessage="error message" value={testValue} onValueChange={(_update) => {}}/>);
// Ensure all error classes set.
expect(tree.find('.usa-input-error')).to.have.lengthOf(1);
const labels = tree.find('.usa-input-error-label');
expect(labels).to.have.lengthOf(1);
expect(labels.text()).to.equal('my label');
const errorMessages = tree.find('.usa-input-error-message');
expect(errorMessages).to.have.lengthOf(1);
expect(errorMessages.text()).to.equal('error message');
// No error means no aria-describedby to not confuse screen readers.
const selects = tree.find('select');
expect(selects).to.have.lengthOf(1);
const idNum = selects.props().id.split('-')[2];
expect(selects.prop('aria-describedby')).to.not.be.undefined;
expect(selects.prop('aria-describedby')).to.equal(`errorable-select-${idNum}-error-message`);
});
it('should pass aXe check when errorMessage is set', () => {
return axeCheck(<ErrorableSelect label="my label" options={options} errorMessage="error message" value={testValue} onValueChange={(_update) => {}}/>);
});
it('required=false does not have required asterisk', () => {
const tree = shallow(<ErrorableSelect label="my label" options={options} value={testValue} onValueChange={(_update) => {}}/>);
expect(tree.find('label').text()).to.equal('my label');
});
it('should pass aXe check when it is not required', () => {
return axeCheck(<ErrorableSelect label="my label" options={options} value={testValue} onValueChange={(_update) => {}}/>);
});
it('required=true has required asterisk', () => {
const tree = shallow(<ErrorableSelect label="my label" options={options} required value={testValue} onValueChange={(_update) => {}}/>);
expect(tree.find('label').text()).to.equal('my label*');
});
it('should pass aXe check when it is required', () => {
return axeCheck(<ErrorableSelect label="my label" options={options} required value={testValue} onValueChange={(_update) => {}}/>);
});
it('label attribute propagates', () => {
const tree = shallow(<ErrorableSelect label="my label" options={options} value={testValue} onValueChange={(_update) => {}}/>);
// Ensure label text is correct.
const labels = tree.find('label');
expect(labels).to.have.lengthOf(1);
expect(labels.text()).to.equal('my label');
// Ensure label htmlFor is attached to select id.
const selects = tree.find('select');
const idNum = selects.props().id.split('-')[2];
expect(selects).to.have.lengthOf(1);
expect(selects.find('id')).to.not.be.undefined;
expect(selects.prop('id')).to.equal(`errorable-select-${idNum}`);
});
});