instacart/Snacks

View on GitHub
src/components/SelectionPills/SelectionPill/SelectionPill.js

Summary

Maintainability
A
2 hrs
Test Coverage
import React from 'react'
import Radium from '@instacart/radium'
import PropTypes from 'prop-types'
import { themePropTypes } from '../../../styles/themer/utils'
import withTheme from '../../../styles/themer/withTheme'
import { getStyles } from './styles'

const NoOp = () => {} // eslint-disable-line no-empty-function

// As of HTML5, id must consist of at least one character and no spaces
const textToID = text => {
  return `selection_pill_${text.split(' ').join('_')}`
}

class SelectionPill extends React.PureComponent {
  static propTypes = {
    /** Unique identifier for the pill. Default to text input if not provided. */
    id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),

    /** Any additional props to add to the list element (e.g. data attributes). */
    listElementAttributes: PropTypes.object,

    /** Any additional props to add to the checkbox element (e.g. data attributes). */
    elementAttributes: PropTypes.object,

    /** Flag determining if component selected state is controlled by parent through props or internal state */
    parentControlledState: PropTypes.bool,

    /** Determines wether or not selected styles are applied and start is a selected state */
    isSelected: PropTypes.bool,

    /** Determines Whether or not the pill is disabled. */
    isDisabled: PropTypes.bool,

    /** Callback function called after pill click
     * @param {SyntheticEvent} event The react `SyntheticEvent`
     * @param {props} object All the props passed to the component
     */
    onClick: PropTypes.func,

    /** Callback function called after pill gained focus
     * @param {SyntheticEvent} event The react `SyntheticEvent`
     * @param {props} object All the props passed to the component
     */
    onFocus: PropTypes.func,

    /** Callback function called after pill has lost focus
     * @param {SyntheticEvent} event The react `SyntheticEvent`
     * @param {props} object All the props passed to the component
     */
    onBlur: PropTypes.func,

    /** Snacks theme attributes provided by Themer */
    snacksTheme: themePropTypes,

    /** Required text to appear inside pill */
    text: PropTypes.string.isRequired,

    /** Optional style overrides for button and its states */
    style: PropTypes.shape({
      button: PropTypes.object,
      disabledStyle: PropTypes.object,
      selectedStyle: PropTypes.object,
      focusedStyle: PropTypes.object,
    }),

    /** Aria overrides for accessibility (i.e. use if label is not descriptive enough for screen readers) */
    aria: PropTypes.shape({
      label: PropTypes.string,
    }),
  }

  static defaultProps = {
    elementAttributes: {},
    isSelected: false,
    parentControlledState: false,
    onClick: NoOp,
    onFocus: NoOp,
    onBlur: NoOp,
    style: {},
    aria: {},
  }

  state = {
    isSelected: this.props.isSelected,
    isFocused: false,
  }

  handleChange = event => {
    const { onClick } = this.props
    const { isSelected } = this.state

    this.setState({ isSelected: !isSelected })
    onClick(event, { ...this.props, isSelected: !isSelected })
  }

  toggleFocus = () => {
    const { isFocused } = this.state
    this.setState({ isFocused: !isFocused })
  }

  handleFocus = event => {
    this.toggleFocus()
    this.props.onFocus(event, { ...this.props, isFocused: !this.state.isFocused })
  }

  handleBlur = event => {
    this.toggleFocus()
    this.props.onBlur(event, { ...this.props, isFocused: !this.state.isFocused })
  }

  renderInputBtn({ selected, inputId }) {
    const { aria, isDisabled } = this.props
    const componentStyles = getStyles()

    return (
      <div style={componentStyles.checkBoxOverrideStyle}>
        <input
          id={inputId}
          type="checkbox"
          onChange={this.handleChange}
          checked={selected}
          disabled={isDisabled}
          aria-label={aria.label}
          onBlur={this.handleBlur}
          onFocus={this.handleFocus}
        />
      </div>
    )
  }

  render() {
    const { id, snacksTheme, text, style, isDisabled, parentControlledState } = this.props
    const { isFocused } = this.state
    const { primaryForeground } = snacksTheme.colors
    const inputId = id || textToID(text)
    const selected = parentControlledState ? this.props.isSelected : this.state.isSelected
    const componentStyles = getStyles({
      isSelected: selected,
      isFocused,
      isDisabled,
      primaryForeground,
      externalStyles: style,
    })

    return (
      <li style={componentStyles.listElement} {...this.props.listElementAttributes}>
        {this.renderInputBtn({ selected, inputId })}
        <label htmlFor={inputId} style={componentStyles.labelButton}>
          {text}
        </label>
      </li>
    )
  }
}

export default withTheme(Radium(SelectionPill))