implement <RadioGroup>

This commit is contained in:
Sérgio Ramos 2016-10-28 00:37:31 +01:00
parent 19236d849d
commit 5eb2b15227
14 changed files with 342 additions and 53 deletions

View File

@ -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,

View File

@ -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')(),
])
}

View File

@ -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"

View 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;

View 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;

View 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>
);
```

View 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;
}
}
}

View File

@ -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;

View File

@ -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

View File

@ -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);
}
}

View File

@ -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'),

View File

@ -9,4 +9,5 @@ module.exports = {
Tabs: require('./components/tabs'),
Toggle: require('./components/toggle'),
Radio: require('./components/radio'),
RadioGroup: require('./components/radio-group')
};

View File

@ -11,4 +11,3 @@ test('renders <Button> without exploding', (t) => {
const wrapper = shallow(<Button />);
t.deepEqual(wrapper.length, 1);
});

View File

@ -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"