Skip to content

Commit 615d4ff

Browse files
Merge pull request #215 from jilliankeenan/PDS-40
(PDS-40) Radiobutton Component
2 parents 7e071bf + 5465f38 commit 615d4ff

File tree

8 files changed

+314
-0
lines changed

8 files changed

+314
-0
lines changed

packages/react-components/source/index.js

+2
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import Tabs from './react/library/tabs';
2828
import Breadcrumb from './react/library/breadcrumb';
2929
import ConfirmationModal from './react/library/confirmation-modal';
3030
import Code from './react/library/code';
31+
import RadioButton from './react/library/radiobutton';
3132

3233
export {
3334
ActionSelect,
@@ -57,4 +58,5 @@ export {
5758
Breadcrumb,
5859
ConfirmationModal,
5960
Code,
61+
RadioButton,
6062
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import PropTypes from 'prop-types';
2+
import React from 'react';
3+
import classNames from 'classnames';
4+
import Icon from '../icon';
5+
6+
import Text from '../text';
7+
8+
const propTypes = {
9+
/** Name of the input */
10+
name: PropTypes.string.isRequired,
11+
/** Human friendly label */
12+
label: PropTypes.string.isRequired,
13+
/** Boolean input value determining if the checkbox is checked or not */
14+
value: PropTypes.bool,
15+
/** Is the input disabled */
16+
disabled: PropTypes.bool,
17+
/** Form error, causing element to render red when present */
18+
error: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
19+
/** Ref method passed to the inner input element */
20+
inputRef: PropTypes.func,
21+
/** Change handler. Passed in order: new value, original event. Additionally, other event handlers and and props are propagated to the inner input element for use as needed */
22+
onChange: PropTypes.func,
23+
/** Custom user-provided className */
24+
className: PropTypes.string,
25+
/** Custom user-provided inline styles */
26+
style: PropTypes.shape({}),
27+
};
28+
29+
const defaultProps = {
30+
value: false,
31+
disabled: false,
32+
error: false,
33+
onChange() {},
34+
inputRef() {},
35+
className: '',
36+
style: {},
37+
};
38+
/** Inner radiobutton dot svg */
39+
const radioDot = {
40+
viewBox: '0 0 16 16',
41+
svg: <circle r="4" cy="8" cx="8" strokeWidth="0" />,
42+
};
43+
/**
44+
* The RadioButton is a lightly styled wrapper around an html radio input.
45+
*/
46+
const RadioButton = ({
47+
name,
48+
value,
49+
label,
50+
error,
51+
className,
52+
style,
53+
inputRef,
54+
onChange,
55+
...otherProps
56+
}) => (
57+
<Text
58+
as="label"
59+
size="small"
60+
htmlFor={name}
61+
className={classNames('rc-radiobutton-input', className)}
62+
style={style}
63+
>
64+
<div className="rc-radiobutton-container">
65+
<input
66+
type="radio"
67+
id={name}
68+
name={name}
69+
checked={value}
70+
ref={inputRef}
71+
className={classNames('rc-radiobutton', {
72+
'rc-radiobutton-error': error,
73+
})}
74+
onChange={e => onChange(e.target.checked, e)}
75+
{...otherProps}
76+
/>
77+
<Icon svg={radioDot.svg} viewBox={radioDot.viewBox} />
78+
</div>
79+
{label}
80+
</Text>
81+
);
82+
83+
RadioButton.propTypes = propTypes;
84+
RadioButton.defaultProps = defaultProps;
85+
86+
export default RadioButton;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
## Overview
2+
3+
The `Radio Button` component is a lightly styled wrapper around an HTML radio input. It leaves most auxiliary functionality to the [`Form.Field`](#/React%20Components/FormField) wrapper. We recommend that in most cases the `Radio Button` is used through the `Form.Field` component to ensure complete design consistency, but there may be some cases in which a pure RadioButton element is desired.
4+
5+
### States and interaction
6+
7+
Radio buttons provide built in support for hover, active, and focused interactions. All radio buttons also provide error and disabled states as needed.
8+
9+
```jsx
10+
const exampleStyle = { marginRight: 10 };
11+
const [value, setValue] = React.useState(0);
12+
13+
<div>
14+
<RadioButton
15+
name="radiobutton-ex-unchecked"
16+
label="Unchecked"
17+
style={exampleStyle}
18+
value={value === 0}
19+
onChange={() => setValue(0)}
20+
/>
21+
<RadioButton
22+
name="radiobutton-ex-checked"
23+
label="Checked"
24+
style={exampleStyle}
25+
value={value === 1}
26+
onChange={() => setValue(1)}
27+
/>
28+
<RadioButton
29+
name="radiobutton-ex-disabled"
30+
label="Disabled"
31+
style={exampleStyle}
32+
value={value === 2}
33+
onChange={() => setValue(2)}
34+
disabled
35+
/>
36+
<RadioButton
37+
name="radiobutton-ex-error"
38+
label="Error"
39+
style={exampleStyle}
40+
value={value === 3}
41+
onChange={() => setValue(3)}
42+
error
43+
/>
44+
</div>;
45+
```
46+
47+
## Basic use
48+
49+
When the radio button is used within a [Form](#Form) component, the value state is either tracked or controlled through the Form component.
50+
51+
### Event handling
52+
53+
When the radio button is used outside of a [Form](#Form) component, the user is responsible for managing value state.
54+
55+
```jsx
56+
<RadioButton
57+
name="radiobutton-ex-event-handling"
58+
label="Radio button label is also clickable"
59+
value={state.checked}
60+
onChange={checked => setState({ checked })}
61+
/>
62+
```
63+
64+
## Related
65+
66+
- [Form](#/React%20Components/Form)
67+
- [Form.Field](#/React%20Components/FormField)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import RadioButton from './RadioButton';
2+
3+
export default RadioButton;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
.rc-radiobutton-input {
2+
align-items: center;
3+
cursor: pointer;
4+
display: flex;
5+
}
6+
7+
.rc-radiobutton-input:not(:last-of-type) {
8+
margin-bottom: $puppet-common-spacing-base * 3;
9+
}
10+
11+
.rc-radiobutton-container {
12+
height: $puppet-common-spacing-base * 4;
13+
margin-right: $puppet-common-spacing-base * 2;
14+
position: relative;
15+
}
16+
17+
// https://github.com/stylelint/stylelint/issues/331#issuecomment-127043187
18+
/* stylelint-disable property-no-vendor-prefix */
19+
/* stylelint-disable length-zero-no-unit */
20+
.rc-radiobutton {
21+
-webkit-appearance: none;
22+
-moz-appearance: none;
23+
appearance: none;
24+
border: $puppet-common-border;
25+
border-radius: $puppet-common-border-radius * 2;
26+
cursor: pointer;
27+
height: $puppet-common-spacing-base * 4;
28+
margin: 0;
29+
outline: none;
30+
padding: 0;
31+
width: $puppet-common-spacing-base * 4;
32+
}
33+
34+
.rc-radiobutton + .rc-icon {
35+
display: none;
36+
height: $puppet-common-spacing-base * 4;
37+
left: 0px;
38+
pointer-events: none;
39+
position: absolute;
40+
top: 0px;
41+
width: $puppet-common-spacing-base * 4;
42+
}
43+
44+
.rc-radiobutton:focus {
45+
box-shadow: $puppet-common-focus-outline;
46+
}
47+
48+
.rc-radiobutton:hover {
49+
border: $puppet-shared-input-border-hover;
50+
}
51+
52+
.rc-radiobutton-error,
53+
.rc-radiobutton-error:hover {
54+
border: $puppet-shared-input-border-error;
55+
}
56+
57+
.rc-radiobutton-error.rc-radiobutton:checked + .rc-icon {
58+
fill: $puppet-r600;
59+
}
60+
61+
.rc-radiobutton:checked + .rc-icon {
62+
display: block;
63+
fill: $puppet-b500;
64+
}
65+
66+
.rc-radiobutton:hover + .rc-icon {
67+
fill: $puppet-b400;
68+
}
69+
70+
.rc-radiobutton:disabled,
71+
.rc-radiobutton:disabled:hover {
72+
border: $puppet-common-border;
73+
cursor: default;
74+
opacity: $puppet-common-disabled-opacity;
75+
76+
+ .rc-icon {
77+
fill: $puppet-b500;
78+
opacity: $puppet-common-disabled-opacity;
79+
}
80+
}
81+
82+
/* stylelint-enable length-zero-no-unit */
83+
/* stylelint-enable property-no-vendor-prefix */

packages/react-components/source/scss/library/components/forms/_checkbox.scss

+4
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@
5454
border: $puppet-shared-input-border-error;
5555
}
5656

57+
.rc-checkbox-error.rc-checkbox:checked + .rc-icon {
58+
fill: $puppet-r600;
59+
}
60+
5761
.rc-checkbox:checked + .rc-icon {
5862
display: block;
5963
fill: $puppet-b500;

packages/react-components/source/scss/library/ui.scss

+1
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,4 @@
2929
@import 'components/table';
3030
@import 'components/breadcrumb';
3131
@import 'components/code';
32+
@import 'components/radiobutton';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import jsdom from 'mocha-jsdom';
2+
import sinon from 'sinon';
3+
import { mount, shallow } from 'enzyme';
4+
import { expect } from 'chai';
5+
import React from 'react';
6+
7+
import RadioButton from '../../source/react/library/radiobutton/RadioButton';
8+
9+
describe('<RadioButton />', () => {
10+
jsdom({ skipWindowCheck: true });
11+
12+
const requiredProps = {
13+
name: 'test-name',
14+
label: 'test-label',
15+
};
16+
17+
it('propagates the provided className to the top level element', () => {
18+
expect(
19+
shallow(<RadioButton {...requiredProps} className="test-class" />),
20+
).to.have.className('test-class');
21+
});
22+
23+
it('propagates provided inline style to the top level element', () => {
24+
expect(
25+
shallow(<RadioButton {...requiredProps} style={{ marginTop: 10 }} />),
26+
).to.have.style('margin-top', '10px');
27+
});
28+
29+
it('should have an accessible ref method to the inner input element', async () => {
30+
const radiobutton = await new Promise(resolve => {
31+
mount(<RadioButton {...requiredProps} inputRef={resolve} />);
32+
});
33+
34+
expect(radiobutton.nodeName).to.equal('INPUT');
35+
});
36+
37+
it('should respond to change if onChange is provided', () => {
38+
const onChange = sinon.spy();
39+
const wrapper = mount(
40+
<RadioButton {...requiredProps} onChange={onChange} />,
41+
);
42+
43+
wrapper.find('input').simulate('change');
44+
45+
expect(onChange.called).to.equal(true);
46+
});
47+
48+
it('should respond to focus if onFocus is provided', () => {
49+
const onFocus = sinon.spy();
50+
const wrapper = mount(<RadioButton {...requiredProps} onFocus={onFocus} />);
51+
52+
wrapper.find('input').simulate('focus');
53+
54+
expect(onFocus.called).to.equal(true);
55+
});
56+
57+
it('should have className corresponding to a present error if provided', () => {
58+
expect(
59+
shallow(<RadioButton {...requiredProps} error />).find('input'),
60+
).to.have.className('rc-radiobutton-error');
61+
});
62+
63+
it('should have an attribute corresponding to being disabled', () => {
64+
expect(shallow(<RadioButton {...requiredProps} disabled />).find('input'))
65+
.to.have.prop('disabled')
66+
.to.equal(true);
67+
});
68+
});

0 commit comments

Comments
 (0)