Source: index.js

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