react-cm-ui
Version:
React UI for Healthy Church
531 lines (490 loc) • 16.1 kB
JSX
import {
clone,
includes,
isArray,
isFunction,
remove,
} from 'lodash';
import ClassNames from 'classnames';
import PropTypes from 'prop-types';
import React from 'react';
import KeyCode from '../../global/keyCode';
import RadioItem from './radioItem';
import withStyles from '../../styles/withStyles';
const propTypes = {
align: PropTypes.oneOf(['left', 'right']),
checked: PropTypes.oneOfType([
PropTypes.bool,
PropTypes.number,
]),
children: PropTypes.node,
classes: PropTypes.shape({
input: PropTypes.string,
isAlignedRight: PropTypes.string,
isChecked: PropTypes.string,
isDisabled: PropTypes.string,
isFluid: PropTypes.string,
isPill: PropTypes.string,
label: PropTypes.string,
labelNotClickable: PropTypes.string,
root: PropTypes.string,
}),
className: PropTypes.string,
/**
* Deprecated prop. Please use `disabled` instead.
*/
disable: PropTypes.bool,
/**
* A Radio can be disabled.
*/
disabled: PropTypes.bool,
fluid: PropTypes.bool,
id: PropTypes.string,
label: PropTypes.string,
labelClick: PropTypes.bool,
multi: PropTypes.bool,
name: PropTypes.string,
onChange: PropTypes.func,
onKeyDown: PropTypes.func,
pill: PropTypes.bool,
style: PropTypes.shape({}),
tabIndex: PropTypes.number,
value: PropTypes.string,
};
const defaultProps = {
align: null,
checked: null,
children: null,
classes: null,
className: null,
disable: false,
disabled: false,
fluid: false,
id: null,
label: null,
labelClick: true,
multi: false,
name: null,
onChange: null,
onKeyDown: null,
pill: false,
style: null,
tabIndex: -1,
value: '',
};
const isCheckedSingle = (id, index, isChecked) => (id === isChecked || index === isChecked);
const isCheckedMulti = (id, index, isChecked) => {
const newId = id || index;
return includes(isChecked, newId);
};
const styles = (theme) => {
const size = 22;
const sizeDot = 14;
const inputStyle = {
display: 'none',
'&:checked +': {
'& $label::after': {
opacity: 1,
},
},
};
return {
isAlignedRight: {},
isDisabled: {},
isFluid: {},
isPill: {},
input: {
...inputStyle,
},
label: {},
labelNotClickable: {},
root: {
display: 'inline-block',
marginRight: theme.spacing(2),
minHeight: size,
outline: 'none',
position: 'relative',
textAlign: 'left',
'&:focus': {
boxShadow: `0 0 0 1px ${theme.palette.active.primary}`,
},
'& .label': {
color: theme.palette.text.primary,
cursor: 'pointer',
display: 'block',
fontSize: 14,
position: 'relative',
'&::before, &::after': {
content: '""',
height: size,
left: 0,
position: 'absolute',
top: 0,
transition: 'opacity 150ms ease',
width: size,
},
'&::before': {
background: theme.palette.background.primary,
border: `1px solid ${theme.palette.border.primary}`,
borderRadius: 11,
},
'&::after': {
backgroundColor: theme.palette.cyan[500],
borderRadius: sizeDot / 2,
height: sizeDot,
margin: (size - sizeDot) / 2,
opacity: 0,
width: sizeDot,
},
'&$labelNotClickable': {
cursor: 'auto',
'&::before, &::after': {
cursor: 'pointer',
},
},
'& > span': {
display: 'inline-block',
paddingLeft: 33,
paddingTop: 2,
},
},
'&$isAlignedRight': {
marginLeft: 11,
marginRight: 0,
textAlign: 'left',
'& .label': {
'&::before, &::after': {
left: 'auto',
right: 0,
},
'& > span': {
paddingLeft: 0,
paddingRight: 33,
},
},
},
'&$isDisabled': {
'& .radio-item .label': {
cursor: 'auto',
'&::before': {
background: theme.palette.background.secondary,
},
'&::after': {
backgroundColor: theme.palette.grey[400],
},
},
'&$isPill': {
'& .radio-item': {
'& .label': {
cursor: 'default',
},
'& .input:checked + .label': {
backgroundColor: theme.palette.grey[400],
borderColor: theme.palette.grey[400],
},
'&:last-child': {
input: {
'&:checked + .label': {
borderRight: `1px solid ${theme.palette.grey[400]}`,
},
},
},
'&.radio-item-is-checked + .radio-item .label': {
borderLeft: `1px solid ${theme.palette.grey[400]}`,
},
},
},
},
'&$isFluid': {
display: 'block',
marginRight: 0,
},
'&$isPill': {
'& .radio-item': {
cursor: 'pointer',
display: 'inline-block',
outline: 'none',
'&:focus': {
boxShadow: `0 0 0 1px ${theme.palette.active.primary}`,
},
'&:first-child:focus': {
borderRadius: '16px 0 0 16px',
},
'&:last-child:focus': {
borderRadius: '0 16px 16px 0',
},
'& .input': {
...inputStyle,
},
'& .label': {
backgroundColor: theme.palette.background.primary,
border: `1px solid ${theme.palette.border.primary}`,
borderRight: 0,
display: 'inline-block',
fontSize: 14,
fontWeight: theme.typography.fontWeightRegular,
outline: 'none',
padding: '6px 22px',
textAlign: 'center',
textDecoration: 'none',
transition: [
[
'background-color',
'125ms',
'linear',
],
[
'border',
'125ms',
'linear',
],
[
'color',
'125ms',
'linear',
],
],
whiteSpace: 'nowrap',
zIndex: -1,
'&::before, &::after': {
display: 'none',
},
},
'& .input:checked + .label': {
backgroundColor: theme.palette.cyan[500],
borderColor: theme.palette.cyan[500],
color: theme.palette.text.contrastText,
},
'&:first-child .label': {
borderRadius: '15.5px 0 0 15.5px',
},
'&:last-child': {
'& .label': {
borderRight: `1px solid ${theme.palette.border.primary}`,
borderRadius: '0 15.5px 15.5px 0',
},
'& .input:checked + .label': {
borderRight: `1px solid ${theme.palette.cyan[500]}`,
},
},
'&.radio-item-is-checked + .radio-item .label': {
borderLeft: `1px solid ${theme.palette.cyan[500]}`,
},
},
},
},
};
};
class Radio extends React.Component {
constructor(props) {
super(props);
this.state = { isChecked: props.checked };
this.onClick = this.onClick.bind(this);
this.onKeyDown = this.onKeyDown.bind(this);
this.onLabelClick = this.onLabelClick.bind(this);
this.onLabelKeyDown = this.onLabelKeyDown.bind(this);
this.onMouseDown = this.onMouseDown.bind(this);
}
componentDidUpdate(prevProps) {
const {
checked: prevChecked,
} = prevProps;
const {
checked,
} = this.props;
if (checked !== prevChecked) {
this.setState({ isChecked: checked });
}
}
onClick(idArg, customValue) {
const {
disable,
disabled,
id,
onChange,
pill,
} = this.props;
const newValue = this.setIsChecked(idArg);
const isNotDisabled = !disable && !disabled;
if (isNotDisabled) {
if (isFunction(onChange)) {
onChange(pill ? idArg : id, newValue, customValue);
} else {
this.setState({ isChecked: newValue });
}
}
}
onKeyDown(event, idArg) {
const {
disable,
disabled,
id,
onKeyDown,
pill,
} = this.props;
if (event.keyCode === KeyCode.Enter) {
const newValue = this.setIsChecked(idArg);
const isNotDisabled = !disable && !disabled;
if (!isNotDisabled) {
if (isFunction(onKeyDown)) {
onKeyDown(pill ? idArg : id, newValue);
} else {
this.setState({ isChecked: newValue });
}
}
}
}
onLabelClick(event) {
const { labelClick } = this.props;
if (labelClick === false) {
event.stopPropagation();
}
}
onLabelKeyDown() {
/**
* NOTE: Need to use a prop function here someday.
*/
return null;
}
onMouseDown(event) {
event.preventDefault();
}
setIsChecked(idArg) {
const {
pill,
multi,
} = this.props;
const {
isChecked,
} = this.state;
let newValue = clone(isChecked);
if (multi) {
if (includes(newValue, idArg)) {
remove(newValue, (v) => v === idArg);
} else if (isArray(newValue)) {
newValue.push(idArg);
} else {
newValue = [idArg];
}
} else if (pill) {
newValue = idArg;
} else {
newValue = true;
}
return newValue;
}
render() {
const {
align,
children,
classes,
className,
disable,
disabled,
fluid,
id,
label,
labelClick,
multi,
name,
pill,
style,
tabIndex,
value,
} = this.props;
const { isChecked } = this.state;
const isDisabled = disable || disabled;
const rootClasses = ClassNames(
'ui',
'radio',
classes.root,
className,
{
[classes.isAlignedRight]: align === 'right',
'radio-align-right': align === 'right',
[classes.isDisabled]: isDisabled,
'radio-disabled': isDisabled,
[classes.isFluid]: fluid,
'radio-full-width': fluid,
[classes.isChecked]: !pill && isChecked,
'radio-is-checked': !pill && isChecked,
[classes.isPill]: pill,
'radio-pill': pill,
},
);
if (pill) {
const isCheckedItem = multi ? isCheckedMulti : isCheckedSingle;
return (
<div
className={rootClasses}
style={style}
>
{React.Children.map(children, (child, index) => React.cloneElement(child, {
checked: isCheckedItem(child.props.id, index, isChecked),
className: child.props.className,
id: child.props.id,
index,
label: child.props.label,
name: !multi ? name : null,
onClick: this.onClick,
onKeyDown: this.onKeyDown,
style: child.props.style,
tabIndex: child.props.tabIndex,
}))}
</div>
);
}
return (
<div
aria-checked={isChecked}
aria-labelledby={id}
className={rootClasses}
onClick={this.onClick}
onKeyDown={this.onKeyDown}
onMouseDown={this.onMouseDown}
role="radio"
style={style}
tabIndex={isDisabled ? -1 : tabIndex}
>
<input
checked={isChecked}
className={ClassNames(
'input',
classes.input,
)}
disabled={isDisabled}
id={id}
name={name}
readOnly
type="radio"
value={value}
/>
{/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
<label
className={ClassNames(
'label',
classes.label,
{
[classes.labelNotClickable]: !labelClick,
'label-not-clickable': labelClick === false,
},
)}
>
{label && (
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
<span
onClick={this.onLabelClick}
onKeyDown={this.onLabelKeyDown}
>
{label}
</span>
)}
</label>
</div>
);
}
}
Radio.Item = RadioItem;
Radio.propTypes = propTypes;
Radio.defaultProps = defaultProps;
export default withStyles(styles)(Radio);