All files / boundless/packages/boundless-checkbox index.js

100% Statements 23/23
100% Branches 24/24
100% Functions 11/11
100% Lines 20/20
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193            5x 1x                                                                                                                                                                                                           24x 2x         5x 1x         3x       4x   3x   3x 1x         3x   2x   2x 1x         29x       29x                                   29x 5x                         29x                      
import React, {PropTypes} from 'react';
import cx from 'classnames';
 
import omit from 'boundless-utils-omit-keys';
import uuid from 'boundless-utils-uuid';
 
const isFunction = (x) => typeof x === 'function';
const noop = () => {};
 
/**
__An accessible checkbox with indeterminate support.__
 
Checkbox is implemented as a "controlled input", meaning it is a direct representation of the model data passed
inside. User interaction will bubble changes in the form of `onChecked` and `onUnchecked` that a controller
view must intercept and apply against the data provider.
 */
export default class Checkbox extends React.PureComponent {
    static propTypes = {
        /**
         * any [React-supported attribute](https://facebook.github.io/react/docs/tags-and-attributes.html#html-attributes)
         */
        '*': PropTypes.any,
 
        /**
         * any valid HTML tag name
         */
        component: PropTypes.string,
 
        /**
         * all input-specific props like `value`, `name`, etc should be passed here -- common ones are listed below
         */
        inputProps: PropTypes.shape({
            /**
             * any [React-supported attribute](https://facebook.github.io/react/docs/tags-and-attributes.html#html-attributes)
             */
            '*': PropTypes.any,
 
            /**
             * determines if the checkbox is rendered as checked/unchecked, see React ["controlled inputs"](https://facebook.github.io/react/docs/forms.html#controlled-components))
             */
            checked: PropTypes.bool,
 
            className: PropTypes.string,
 
            /**
             * prevents the `on{Checked,Unchecked}` events from firing when `true`
             */
            disabled: PropTypes.bool,
            id: PropTypes.string,
 
            /**
             * enables or disables "mixed" checkbox state, read this [CSS-Tricks article](https://css-tricks.com/indeterminate-checkboxes/)  for more information and examples
             */
            indeterminate: PropTypes.bool,
 
            onChange: PropTypes.func,
            onClick: PropTypes.func,
 
            /**
             * rendered as the input control's form name
             */
            name: PropTypes.string,
 
            /**
             *  passed-through to the input node, like `name`
             */
            value: PropTypes.string,
        }),
 
        /**
         * any React-renderable content, most commonly a simple string
         */
        label: PropTypes.node,
 
        labelProps: PropTypes.shape({
            /**
             * any [React-supported attribute](https://facebook.github.io/react/docs/tags-and-attributes.html#html-attributes)
             */
            '*': PropTypes.any,
        }),
 
        /**
         * called when the element becomes checked; backing data must be updated to persist the state change
         */
        onChecked: PropTypes.func,
 
        /**
         * called when the element becomes unchecked; backing data must be updated to persist the state change
         */
        onUnchecked: PropTypes.func,
    }
 
    static defaultProps = {
        component: 'div',
        inputProps: {
            checked: false,
            indeterminate: false,
        },
        label: null,
        labelProps: {},
        onChecked: noop,
        onUnchecked: noop,
    }
 
    static internalKeys = Object.keys(Checkbox.defaultProps)
 
    id = uuid()
 
    componentDidMount() {
        if (this.props.inputProps.indeterminate) {
            this.setIndeterminate();
        }
    }
 
    componentDidUpdate(prevProps) {
        if (prevProps.inputProps.indeterminate !== this.props.inputProps.indeterminate) {
            this.setIndeterminate();
        }
    }
 
    setIndeterminate() {
        this.refs.input.indeterminate = !!this.props.inputProps.indeterminate;
    }
 
    handleChange = (event) => { // Send the opposite signal from what was passed to toggle the data
        if (this.props.inputProps.disabled) { return; }
 
        this.props[!this.props.inputProps.checked ? 'onChecked' : 'onUnchecked'](this.props.inputProps.name);
 
        if (isFunction(this.props.inputProps.onChange)) {
            this.props.inputProps.onChange(event);
        }
    }
 
    handleClick = (event) => {
        if (this.props.inputProps.disabled) { return; }
 
        this.refs.input.focus();
 
        if (isFunction(this.props.inputProps.onClick)) {
            this.props.inputProps.onClick(event);
        }
    }
 
    getAriaState() {
        return this.props.inputProps.indeterminate ? 'mixed' : String(this.props.inputProps.checked);
    }
 
    renderInput() {
        return (
            <input
                {...omit(this.props.inputProps, 'indeterminate')}
                ref='input'
                type='checkbox'
                className={cx('b-checkbox', this.props.inputProps.className, {
                    'b-checkbox-mixed': this.props.inputProps.indeterminate,
                    'b-checkbox-checked': this.props.inputProps.checked,
                    'b-checkbox-unchecked': !this.props.inputProps.indeterminate && !this.props.inputProps.checked,
                })}
                id={this.props.inputProps.id || this.id}
                aria-checked={this.getAriaState()}
                onChange={this.handleChange}
                onClick={this.handleClick} />
        );
    }
 
    renderLabel() {
        if (this.props.label) {
            return (
                <label
                    {...this.props.labelProps}
                    ref='label'
                    className={cx('b-checkbox-label', this.props.labelProps.className)}
                    htmlFor={this.props.inputProps.id || this.id}>
                    {this.props.label}
                </label>
            );
        }
    }
 
    render() {
        return (
            <this.props.component
                {...omit(this.props, Checkbox.internalKeys)}
                ref='wrapper'
                className={cx('b-checkbox-wrapper', this.props.className)}>
                {this.renderInput()}
                {this.renderLabel()}
            </this.props.component>
        );
    }
}