implement <RadioGroup>
This commit is contained in:
parent
19236d849d
commit
5eb2b15227
@ -77,7 +77,7 @@
|
||||
"jsx-a11y/aria-proptypes": 2,
|
||||
"jsx-a11y/aria-role": 2,
|
||||
"jsx-a11y/aria-unsupported-elements": 2,
|
||||
"jsx-a11y/click-events-have-key-events": 2,
|
||||
"jsx-a11y/click-events-have-key-events": 1,
|
||||
"jsx-a11y/mouse-events-have-key-events": 2,
|
||||
"jsx-a11y/heading-has-content": 2,
|
||||
"jsx-a11y/html-has-lang": 2,
|
||||
@ -88,7 +88,7 @@
|
||||
"jsx-a11y/no-access-key": 2,
|
||||
"jsx-a11y/no-marquee": 2,
|
||||
"jsx-a11y/no-onchange": 2,
|
||||
"jsx-a11y/no-static-element-interactions": 2,
|
||||
"jsx-a11y/no-static-element-interactions": 1,
|
||||
"jsx-a11y/onclick-has-focus": 2,
|
||||
"jsx-a11y/onclick-has-role": 2,
|
||||
"jsx-a11y/role-has-required-aria-props": 2,
|
||||
|
@ -1,12 +0,0 @@
|
||||
var calc = require('postcss-calc');
|
||||
|
||||
module.exports = function (postcss) {
|
||||
return postcss([
|
||||
require('postcss-at-rules-variables')(),
|
||||
require('postcss-modules-values'),
|
||||
require("postcss-import"),
|
||||
require('postcss-mixins')(),
|
||||
require('postcss-for'),
|
||||
require('postcss-cssnext')(),
|
||||
])
|
||||
}
|
@ -13,7 +13,12 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"classnames": "^2.2.5",
|
||||
"invariant": "^2.2.1",
|
||||
"lodash.find": "^4.6.0",
|
||||
"lodash.first": "^3.0.0",
|
||||
"lodash.flatten": "^4.4.0",
|
||||
"lodash.get": "^4.4.2",
|
||||
"lodash.isundefined": "^3.0.1",
|
||||
"param-case": "^2.1.0",
|
||||
"react": "^15.3.2",
|
||||
"react-a11y": "^0.3.3"
|
||||
|
163
ui/src/components/radio-group/index.js
Normal file
163
ui/src/components/radio-group/index.js
Normal file
@ -0,0 +1,163 @@
|
||||
/* After some time tring to make this work without messing w/ checked property,
|
||||
* I ended up using it *only* when none is defined
|
||||
*
|
||||
* This way we try to be as pure as possible and not mess with consumer's logic
|
||||
* if they have any
|
||||
*/
|
||||
|
||||
const first = require('lodash.first');
|
||||
const isUndefined = require('lodash.isundefined');
|
||||
const get = require('lodash.get');
|
||||
const invariant = require('invariant');
|
||||
const Item = require('./item');
|
||||
const find = require('lodash.find');
|
||||
const classNames = require('classnames');
|
||||
const React = require('react');
|
||||
const styles = require('./style.css');
|
||||
|
||||
const RadioGroup = React.createClass({
|
||||
propTypes: {
|
||||
children: React.PropTypes.node,
|
||||
className: React.PropTypes.string,
|
||||
id: React.PropTypes.string,
|
||||
name: React.PropTypes.string.isRequired,
|
||||
onChange: React.PropTypes.func,
|
||||
style: React.PropTypes.object
|
||||
},
|
||||
getInitialState: function() {
|
||||
return this.getState(this.props);
|
||||
},
|
||||
componentWillMount: function() {
|
||||
return this.checkValues(this.props);
|
||||
},
|
||||
componentWillReceiveProps: function(nextProps) {
|
||||
return this.setState(this.checkValues(nextProps));
|
||||
},
|
||||
getState: function(props) {
|
||||
const _children = React.Children.toArray(props.children).filter((child) => {
|
||||
return get(child, 'type.displayName') === 'Radio';
|
||||
});
|
||||
|
||||
const hasChecked = _children.some((child) => {
|
||||
return !isUndefined(get(child, 'props.checked'));
|
||||
});
|
||||
|
||||
if (hasChecked) {
|
||||
return {
|
||||
hasChecked
|
||||
};
|
||||
}
|
||||
|
||||
const defaultChecked = get(find(_children, (child) => {
|
||||
return get(child, 'props.defaultChecked');
|
||||
}), 'props.value');
|
||||
|
||||
const checked = (() => {
|
||||
const stateChecked = get(this, 'state.checked');
|
||||
const fallback = isUndefined(defaultChecked)
|
||||
? get(first(_children), 'props.value')
|
||||
: defaultChecked;
|
||||
|
||||
return !isUndefined(stateChecked) ? stateChecked : fallback;
|
||||
})();
|
||||
|
||||
return {
|
||||
checked
|
||||
};
|
||||
},
|
||||
checkValues: function(props) {
|
||||
invariant(React.Children.toArray(props.children).every((child) => {
|
||||
return (child.type.name !== 'Radio') ? true : !!child.props.value;
|
||||
}), 'All <Radio> childs of <RadioGroup> must have a value property');
|
||||
|
||||
return props;
|
||||
},
|
||||
handleChange: function(key) {
|
||||
return (ev) => {
|
||||
const {
|
||||
onChange = () => {}
|
||||
} = this.props;
|
||||
|
||||
this.setState({
|
||||
checked: key
|
||||
}, () => {
|
||||
onChange(ev);
|
||||
});
|
||||
};
|
||||
},
|
||||
render: function() {
|
||||
const {
|
||||
name,
|
||||
children,
|
||||
className,
|
||||
id,
|
||||
style
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
hasChecked,
|
||||
checked
|
||||
} = this.state;
|
||||
|
||||
const {
|
||||
handleChange
|
||||
} = this;
|
||||
|
||||
const cn = classNames(
|
||||
className,
|
||||
styles.group
|
||||
);
|
||||
|
||||
const _children = React.Children.map(children, (child, i) => {
|
||||
if (child.type.name !== 'Radio') {
|
||||
return child;
|
||||
}
|
||||
|
||||
const tabIndex = i + 1;
|
||||
const disabled = get(child, 'props.disabled');
|
||||
const value = get(child, 'props.value');
|
||||
|
||||
const _handleChange = (!hasChecked && !disabled)
|
||||
? handleChange(value)
|
||||
: undefined;
|
||||
|
||||
const _child = hasChecked ? (
|
||||
React.cloneElement(child, {
|
||||
name
|
||||
})
|
||||
) : (
|
||||
React.cloneElement(child, {
|
||||
onChange: _handleChange,
|
||||
checked: value === checked,
|
||||
defaultChecked: undefined,
|
||||
name
|
||||
})
|
||||
);
|
||||
|
||||
const _checked = get(_child, 'props.checked');
|
||||
|
||||
return (
|
||||
<Item
|
||||
checked={_checked}
|
||||
disabled={disabled}
|
||||
onClick={_handleChange}
|
||||
tabIndex={tabIndex}
|
||||
>
|
||||
{_child}
|
||||
</Item>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn}
|
||||
id={id}
|
||||
style={style}
|
||||
>
|
||||
{_children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = RadioGroup;
|
40
ui/src/components/radio-group/item.js
Normal file
40
ui/src/components/radio-group/item.js
Normal file
@ -0,0 +1,40 @@
|
||||
const classNames = require('classnames');
|
||||
const React = require('react');
|
||||
const styles = require('./style.css');
|
||||
|
||||
const Item = ({
|
||||
children,
|
||||
checked = false,
|
||||
disabled = false,
|
||||
onClick,
|
||||
tabIndex
|
||||
}) => {
|
||||
const cn = classNames(
|
||||
styles.item,
|
||||
disabled ? styles.disabled : '',
|
||||
checked ? styles.checked : ''
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
aria-checked={checked}
|
||||
aria-disabled={disabled}
|
||||
className={cn}
|
||||
onClick={onClick}
|
||||
role='radio'
|
||||
tabIndex={tabIndex}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Item.propTypes = {
|
||||
checked: React.PropTypes.bool,
|
||||
children: React.PropTypes.node,
|
||||
disabled: React.PropTypes.bool,
|
||||
onClick: React.PropTypes.func,
|
||||
tabIndex: React.PropTypes.number
|
||||
};
|
||||
|
||||
module.exports = Item;
|
35
ui/src/components/radio-group/readme.md
Normal file
35
ui/src/components/radio-group/readme.md
Normal file
@ -0,0 +1,35 @@
|
||||
# `<RadioGroup>`
|
||||
|
||||
## demo
|
||||
|
||||
```embed
|
||||
const React = require('react');
|
||||
const ReactDOM = require('react-dom/server');
|
||||
const Base = require('../base');
|
||||
const Container = require('../container');
|
||||
const Row = require('../row');
|
||||
const Column = require('../column');
|
||||
const Radio = require('../radio');
|
||||
const RadioGroup = require('./index');
|
||||
const styles = require('./style.css');
|
||||
|
||||
nmodule.exports = ReactDOM.renderToString(
|
||||
<Base>
|
||||
<Row>
|
||||
<Column>
|
||||
<RadioGroup name='mode'>
|
||||
<Radio label='Default settings' value='default'>
|
||||
<p>You get all the good bits and none of the rubbish</p>
|
||||
</Radio>
|
||||
<Radio label='Fancy settings' value='fancy'>
|
||||
<p>You get all the good bits and extra brownies</p>
|
||||
</Radio>
|
||||
<Radio disabled label='No settings' value='none'>
|
||||
<p>You get none of the good bits</p>
|
||||
</Radio>
|
||||
</RadioGroup>
|
||||
</Column>
|
||||
</Row>
|
||||
</Base>
|
||||
);
|
||||
```
|
24
ui/src/components/radio-group/style.css
Normal file
24
ui/src/components/radio-group/style.css
Normal file
@ -0,0 +1,24 @@
|
||||
.group {
|
||||
& .item {
|
||||
cursor: pointer;
|
||||
background: #FFFFFF;
|
||||
border: 1px solid #D8D8D8;
|
||||
box-shadow: 0px 2px 0px 0px rgba(0,0,0,0.05);
|
||||
border-radius: 4px;
|
||||
margin-bottom: 15px;
|
||||
padding: 25px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: initial;
|
||||
}
|
||||
|
||||
&.checked {
|
||||
border: 1px solid #1D35BC;
|
||||
box-shadow: 0px 2px 0px 0px rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
}
|
@ -3,48 +3,66 @@ const React = require('react');
|
||||
const styles = require('./style.css');
|
||||
|
||||
const Radio = ({
|
||||
name,
|
||||
value,
|
||||
label,
|
||||
checked,
|
||||
disabled = false,
|
||||
children,
|
||||
className,
|
||||
defaultChecked,
|
||||
disabled = false,
|
||||
id,
|
||||
label,
|
||||
name,
|
||||
onChange,
|
||||
style
|
||||
style,
|
||||
value
|
||||
}) => {
|
||||
const _label = label || children;
|
||||
const _children = label ? children : null;
|
||||
|
||||
const cn = classNames(
|
||||
className,
|
||||
styles.radio
|
||||
);
|
||||
|
||||
const labelledby = `${styles.label}-label`;
|
||||
|
||||
return (
|
||||
<label className={cn} htmlFor={id}>
|
||||
<input
|
||||
checked={checked}
|
||||
disabled={disabled}
|
||||
id={id}
|
||||
name={name}
|
||||
onChange={onChange}
|
||||
type='radio'
|
||||
value={value}
|
||||
/>
|
||||
<span>{label}</span>
|
||||
</label>
|
||||
<div className={cn}>
|
||||
<label className={styles.label} htmlFor={id}>
|
||||
<input
|
||||
aria-labelledby={labelledby}
|
||||
checked={checked}
|
||||
className={styles.input}
|
||||
defaultChecked={defaultChecked}
|
||||
disabled={disabled}
|
||||
id={id}
|
||||
name={name}
|
||||
onChange={onChange}
|
||||
type='radio'
|
||||
value={value}
|
||||
/>
|
||||
<span className={styles.span} id={labelledby}>
|
||||
{_label}
|
||||
</span>
|
||||
</label>
|
||||
<span>
|
||||
{_children}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Radio.propTypes = {
|
||||
checked: React.PropTypes.bool,
|
||||
children: React.PropTypes.node,
|
||||
className: React.PropTypes.string,
|
||||
defaultChecked: React.PropTypes.bool,
|
||||
disabled: React.PropTypes.bool,
|
||||
id: React.PropTypes.string,
|
||||
label: React.PropTypes.node,
|
||||
name: React.PropTypes.string,
|
||||
label: React.PropTypes.string,
|
||||
name: React.PropTypes.string.isRequired,
|
||||
onChange: React.PropTypes.func,
|
||||
style: React.PropTypes.object,
|
||||
value: React.PropTypes.string
|
||||
value: React.PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
module.exports = Radio;
|
||||
|
@ -16,15 +16,18 @@ nmodule.exports = ReactDOM.renderToString(
|
||||
<Base>
|
||||
<Row>
|
||||
<Column>
|
||||
<Radio name='gender' value='female' label='Female' checked={false}/>
|
||||
<Radio name='gender' value='male' label='Male' checked={true}/>
|
||||
<Radio name='gender' value='female' checked={false}>
|
||||
Female
|
||||
</Radio>
|
||||
<Radio name='gender' value='male' checked={true}>
|
||||
Male
|
||||
</Radio>
|
||||
</Column>
|
||||
</Row>
|
||||
</Base>
|
||||
);
|
||||
```
|
||||
|
||||
|
||||
## usage
|
||||
|
||||
```js
|
||||
|
@ -10,11 +10,11 @@
|
||||
.radio {
|
||||
cursor: pointer;
|
||||
|
||||
label {
|
||||
& .label {
|
||||
display: block;
|
||||
}
|
||||
|
||||
& span {
|
||||
& .span {
|
||||
margin-left: remCalc(30);
|
||||
margin-right: remCalc(20);
|
||||
|
||||
@ -25,11 +25,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
& input[type="radio"] {
|
||||
& .input[type="radio"] {
|
||||
display: none;
|
||||
visibility: hidden;
|
||||
|
||||
&:checked ~ span:before {
|
||||
&:checked ~ .span:before {
|
||||
background: var(--dot-color);
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ module.exports = {
|
||||
Container: require('./components/container/readme.md'),
|
||||
Row: require('./components/row/readme.md'),
|
||||
Radio: require('./components/radio/readme.md'),
|
||||
'Radio Group': require('./components/radio-group/readme.md'),
|
||||
Column: require('./components/column/readme.md'),
|
||||
Button: require('./components/button/readme.md'),
|
||||
Toggle: require('./components/toggle/readme.md'),
|
||||
|
@ -9,4 +9,5 @@ module.exports = {
|
||||
Tabs: require('./components/tabs'),
|
||||
Toggle: require('./components/toggle'),
|
||||
Radio: require('./components/radio'),
|
||||
RadioGroup: require('./components/radio-group')
|
||||
};
|
||||
|
@ -11,4 +11,3 @@ test('renders <Button> without exploding', (t) => {
|
||||
const wrapper = shallow(<Button />);
|
||||
t.deepEqual(wrapper.length, 1);
|
||||
});
|
||||
|
||||
|
34
ui/yarn.lock
34
ui/yarn.lock
@ -1701,8 +1701,8 @@ create-hmac@^1.1.0, create-hmac@^1.1.2:
|
||||
inherits "^2.0.1"
|
||||
|
||||
cross-spawn-async@^2.0.0:
|
||||
version "2.2.4"
|
||||
resolved "https://registry.yarnpkg.com/cross-spawn-async/-/cross-spawn-async-2.2.4.tgz#c9a8d8e9a06502c7a46296e33a1a054b5d2f1812"
|
||||
version "2.2.5"
|
||||
resolved "https://registry.yarnpkg.com/cross-spawn-async/-/cross-spawn-async-2.2.5.tgz#845ff0c0834a3ded9d160daca6d390906bb288cc"
|
||||
dependencies:
|
||||
lru-cache "^4.0.0"
|
||||
which "^1.2.8"
|
||||
@ -2955,7 +2955,7 @@ interpret@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.0.1.tgz#d579fb7f693b858004947af39fa0db49f795602c"
|
||||
|
||||
invariant@^2.2.0, invariant@^2.2.1:
|
||||
invariant, invariant@^2.2.0, invariant@^2.2.1:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.1.tgz#b097010547668c7e337028ebe816ebe36c8a8d54"
|
||||
dependencies:
|
||||
@ -3462,6 +3462,14 @@ lodash.filter@^4.4.0:
|
||||
version "4.6.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.filter/-/lodash.filter-4.6.0.tgz#668b1d4981603ae1cc5a6fa760143e480b4c4ace"
|
||||
|
||||
lodash.find:
|
||||
version "4.6.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.find/-/lodash.find-4.6.0.tgz#cb0704d47ab71789ffa0de8b97dd926fb88b13b1"
|
||||
|
||||
lodash.first:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.first/-/lodash.first-3.0.0.tgz#5dae180d7f818ee65fc5b210b104a7bbef98a16a"
|
||||
|
||||
lodash.flatten@^4.2.0, lodash.flatten@^4.4.0:
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f"
|
||||
@ -3470,7 +3478,7 @@ lodash.foreach@^4.3.0:
|
||||
version "4.5.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.foreach/-/lodash.foreach-4.5.0.tgz#1a6a35eace401280c7f06dddec35165ab27e3e53"
|
||||
|
||||
lodash.get@^4.4.2:
|
||||
lodash.get:
|
||||
version "4.4.2"
|
||||
resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"
|
||||
|
||||
@ -3478,6 +3486,10 @@ lodash.indexof@^4.0.5:
|
||||
version "4.0.5"
|
||||
resolved "https://registry.yarnpkg.com/lodash.indexof/-/lodash.indexof-4.0.5.tgz#53714adc2cddd6ed87638f893aa9b6c24e31ef3c"
|
||||
|
||||
lodash.isundefined:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/lodash.isundefined/-/lodash.isundefined-3.0.1.tgz#23ef3d9535565203a66cefd5b830f848911afb48"
|
||||
|
||||
lodash.map@^4.4.0:
|
||||
version "4.6.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.map/-/lodash.map-4.6.0.tgz#771ec7839e3473d9c4cde28b19394c3562f4f6d3"
|
||||
@ -4415,7 +4427,7 @@ postcss-for@^2.1.1:
|
||||
postcss "^5.0.0"
|
||||
postcss-simple-vars "^2.0.0"
|
||||
|
||||
postcss-functions:
|
||||
postcss-functions@^2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/postcss-functions/-/postcss-functions-2.1.1.tgz#f9b64d3b5690f6795fe42a180496805375b7a840"
|
||||
dependencies:
|
||||
@ -4449,13 +4461,13 @@ postcss-js@^0.1.3:
|
||||
postcss "^5.0.21"
|
||||
|
||||
postcss-load-config@^1.0.0-rc:
|
||||
version "1.0.0-rc"
|
||||
resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-1.0.0-rc.tgz#8aed0d0fb94afe2c1ab0ba2ca69da3af5079e2cc"
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-1.0.0.tgz#1399f60dcd6bd9c3124b2eb22960f77f9dc08b3d"
|
||||
dependencies:
|
||||
cosmiconfig "^2.1.0"
|
||||
object-assign "^4.1.0"
|
||||
postcss-load-options "^1.0.2"
|
||||
postcss-load-plugins "^2.0.0-rc"
|
||||
postcss-load-plugins "^2.0.0"
|
||||
|
||||
postcss-load-options@^1.0.2:
|
||||
version "1.0.2"
|
||||
@ -4464,7 +4476,7 @@ postcss-load-options@^1.0.2:
|
||||
cosmiconfig "^2.1.0"
|
||||
object-assign "^4.1.0"
|
||||
|
||||
postcss-load-plugins@^2.0.0-rc:
|
||||
postcss-load-plugins@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/postcss-load-plugins/-/postcss-load-plugins-2.0.0.tgz#2840d8df1d1c57ebcb1d41b5f60d45796504b43f"
|
||||
dependencies:
|
||||
@ -5710,8 +5722,8 @@ trim-newlines@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613"
|
||||
|
||||
tryit@^1.0.1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/tryit/-/tryit-1.0.2.tgz#c196b0073e6b1c595d93c9c830855b7acc32a453"
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/tryit/-/tryit-1.0.3.tgz#393be730a9446fd1ead6da59a014308f36c289cb"
|
||||
|
||||
tty-browserify@0.0.0:
|
||||
version "0.0.0"
|
||||
|
Loading…
Reference in New Issue
Block a user