import React from 'react';
import PropTypes from 'prop-types';
export const CheckboxGroupContext = React.createContext(undefined);
/**
* @typedef {(string|number|boolean)} CheckboxValue
*/
/**
* @typedef {Object} CheckboxOptions
* @property {?boolean} checked - Whether or not the checkbox is checked.
* @property {?boolean} indeterminate - Whether or not the checkbox is indeterminate.
* @property {?Function} onChange - The function to call when the checkbox input changes state.
*/
/**
* @typedef {Object} ExtractedContext
* @property {string} name - The name of the checkbox group.
* @property {CheckboxOptions} options - The natural attributes of a checkbox input.
* @property {...*} props - Any additional props to pass to a checkbox input.
*/
/**
* Extracts checkbox group context information for a given checkbox value into a normal props object.
*
* @param {{name: string, checkedValues: array.<CheckboxValue>, indeterminateValues: array.<CheckboxValue>, onChange: Function}} contextValue - The context information to extract.
* @param {CheckboxValue} checkboxValue - The value of the checkbox to extract context for.
* @param {object} props - Normal props to pass to the individual checkbox.
* @returns {ExtractedContext}
*/
const extractContext = (contextValue, checkboxValue, props) => {
// For some reason if we destructure values in the params hot reloading doesn't work
const { name, checkedValues, indeterminateValues, onChange } = contextValue;
const options = {};
if (checkedValues) {
options.checked = checkedValues.indexOf(checkboxValue) >= 0;
}
if (indeterminateValues) {
options.indeterminate = indeterminateValues.indexOf(checkboxValue) >= 0 ? true : undefined;
}
if (typeof onChange === 'function') {
options.onChange = onChange.bind(null, checkboxValue);
}
return { name, options, ...props };
};
/**
* A simple stateless functional component that renders a checkbox.
*
* @param {Object} props The props object for the component.
* @param {CheckboxValue} props.value The value of the checkbox.
* @param {boolean} props.indeterminate Whether or not the checkbox is indeterminate.
* @param {...Object} props.restProps Any additional props for the checkbox input element.
* @constructor
*/
const PreContextualizedCheckbox = ({ value, indeterminate, ...restProps }) => (
<input
aria-checked={indeterminate ? 'mixed' : restProps.checked.toString()}
type="checkbox"
ref={elem => {
if (elem) {
// eslint-disable-next-line no-param-reassign
elem.indeterminate = indeterminate ? 'true' : undefined;
}
}}
{...restProps}
/>
);
PreContextualizedCheckbox.defaultProps = {
value: undefined,
name: undefined,
checked: false,
indeterminate: undefined,
onChange: undefined,
};
PreContextualizedCheckbox.propTypes = {
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool]),
name: PropTypes.string,
checked: PropTypes.bool,
indeterminate: PropTypes.bool,
onChange: PropTypes.func,
};
export const Checkbox = props => (
<CheckboxGroupContext.Consumer>
{value => {
const { name, options, ...rest } = extractContext(value, props.value, props);
return (
<PreContextualizedCheckbox
name={name}
checked={options.checked}
indeterminate={options.indeterminate}
onChange={options.onChange}
{...rest}
/>
);
}}
</CheckboxGroupContext.Consumer>
);
Checkbox.defaultProps = {
value: undefined,
};
Checkbox.propTypes = {
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool]),
};
export class CheckboxGroup extends React.Component {
state = {
checkedValues: this.props.checkedValues,
indeterminateValues: this.props.indeterminateValues,
};
componentWillReceiveProps(newProps) {
if (newProps.checkedValues) {
this.setState({
checkedValues: newProps.checkedValues,
});
}
if (newProps.indeterminateValues) {
this.setState({
indeterminateValues: newProps.indeterminateValues,
});
}
}
isControlledComponent = () => Boolean(this.props.checkedValues);
onCheckboxChange = (checkboxValue, event) => {
let newValues;
if (this.state.checkedValues.includes(checkboxValue)) {
newValues = this.state.checkedValues.filter(v => v !== checkboxValue);
} else {
newValues = this.state.checkedValues.concat(checkboxValue);
}
if (this.isControlledComponent()) {
this.setState({ checkedValues: this.props.checkedValues });
} else {
this.setState({ checkedValues: newValues });
}
if (typeof this.props.onChange === 'function') {
this.props.onChange(newValues, event, this.props.name);
}
};
onCheckboxIndeterminate = (checkboxValue, event) => {
let newValues;
if (this.state.indeterminateValues.includes(checkboxValue)) {
newValues = this.state.indeterminateValues.filter(v => v !== checkboxValue);
} else {
newValues = this.state.indeterminateValues.concat(checkboxValue);
}
if (this.isControlledComponent()) {
this.setState({ indeterminateValues: this.props.indeterminateValues });
} else {
this.setState({ indeterminateValues: newValues });
}
if (typeof this.props.onIndeterminate === 'function') {
this.props.onIndeterminate(newValues, event, this.props.name);
}
};
createChildren = (values, renderer, providedValue) =>
values.map((value, index) => {
let checkboxValue = value;
let props;
let label;
// value may be the direct value or an object with a value prop.
if (typeof value === 'object') {
checkboxValue = value.value;
props = value.props ? value.props : {};
label = value.label ? value.label : checkboxValue;
}
const { name, options, ...rest } = extractContext(providedValue, checkboxValue, props);
// label may be a function to return the label string or just the label string
if (label && {}.toString.call(label) === '[object Function]') {
label = label();
} else if (!label) {
label = checkboxValue;
}
const CheckboxComponent = (
<PreContextualizedCheckbox
key={[checkboxValue, index].join(' ')}
name={name}
checked={options.checked}
indeterminate={options.indeterminate}
onChange={options.onChange}
{...rest}
/>
);
return renderer(CheckboxComponent, index, {
label,
value: checkboxValue,
name,
options,
...props,
});
});
render() {
const {
Component,
name,
values,
checkedValues,
indeterminateValues,
onChange,
onIndeterminate,
children,
checkboxRenderer,
...rest
} = this.props;
const providedValue = {
name,
checkedValues,
indeterminateValues,
onChange: this.onCheckboxChange,
toggleIndeterminate: this.onCheckboxIndeterminate,
};
return (
<CheckboxGroupContext.Provider value={providedValue}>
<Component {...rest}>
{children || this.createChildren(values, checkboxRenderer, providedValue)}
</Component>
</CheckboxGroupContext.Provider>
);
}
}
// Tbe shape of a checkbox value
const valueShape = {
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool]).isRequired,
label: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
props: PropTypes.objectOf(PropTypes.any),
};
valueShape.children = PropTypes.arrayOf(PropTypes.shape(valueShape));
CheckboxGroup.defaultProps = {
name: undefined,
values: [],
checkedValues: [],
indeterminateValues: [],
onChange: undefined,
onIndeterminate: undefined,
children: undefined,
Component: 'div',
checkboxRenderer: (CheckboxComponent, index, { value, label }) => (
<label key={[value, index].join(' ')}>
{CheckboxComponent} {label}
</label>
),
};
CheckboxGroup.propTypes = {
name: PropTypes.string,
values: PropTypes.arrayOf(
PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
PropTypes.bool,
PropTypes.shape(valueShape),
])
),
// values: PropTypes.arrayOf(PropTypes.shape(valueShape)),
checkedValues: PropTypes.arrayOf(
PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool])
),
indeterminateValues: PropTypes.arrayOf(
PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool])
),
onChange: PropTypes.func,
onIndeterminate: PropTypes.func,
children: PropTypes.node,
Component: PropTypes.node,
checkboxRenderer: PropTypes.func,
};