@muvehealth/fixins
Version: 
Component library for Muvehealth
230 lines (209 loc) • 6.28 kB
Flow
// @flow
import Downshift from 'downshift'
import React, { PureComponent } from 'react'
import { contains, map, pluck } from 'ramda'
import ErrorMessage from '../ErrorMessage'
import InputWrapper from '../MSInputWrapper'
import Item from '../MSItem'
import Label from '../Label'
import Menu from '../MSMenu'
import TagItem from '../MSTagItem'
import Relative from '../Relative'
import { mapIndexed } from '../../utils'
import { type SelectedItemType, type EventType, type ChangesType } from '../../types'
type Props = {
  handleChangeValue: (Array<SelectedItemType>,
    SelectedItemType | Array<SelectedItemType>) => void | () => void,
  input: {
    value: Array<string>,
  },
  label: string,
  meta: {
    touched: boolean,
    error: string,
    dispatch: (() => void) => void,
  },
  name: string,
  options: Array<{
    value: string,
    label: string,
  }>,
}
type State = {
  selectedItems: Array<SelectedItemType>,
  isOpen: boolean,
}
const getSelectedItems = (props) => {
  let items = []
  const { input: { value } } = props
  if (value) {
    items = map(val => ({ label: val, tag: val, value: val }))(value)
  }
  return items
}
class Multiselect extends PureComponent<Props, State> {
  input = null
  inputWrapper = null
  constructor(props: Props) {
    super()
    this.state = {
      selectedItems: getSelectedItems(props),
      isOpen: false,
    }
  }
  onChange = (
    selectedItem: SelectedItemType,
    other: { selectedItem: SelectedItemType | Array<SelectedItemType> },
  ) => {
    const { selectedItems } = this.state
    if (selectedItems.includes(selectedItem)) {
      this.removeItem(selectedItem, other.selectedItem)
    } else {
      this.addItem(selectedItem, other.selectedItem)
    }
  }
  onWrapperClick = (e: EventType) => {
    if (this.inputWrapper === e.target || this.input === e.target) {
      this.focusOnInput()
      e.stopPropagation()
      e.preventDefault()
    }
  }
  addItem = (value: SelectedItemType, prevState: SelectedItemType | Array<SelectedItemType>) => {
    this.setState(({ selectedItems }, props) => {
      const newValues = [...selectedItems, value]
      props.handleChangeValue(newValues, prevState)
      return { selectedItems: newValues }
    })
  }
  removeItem = (value: SelectedItemType, prevState: SelectedItemType | Array<SelectedItemType>) => {
    this.setState(({ selectedItems }, props) => {
      const newValues = selectedItems.filter(i => i !== value)
      props.handleChangeValue(newValues, prevState)
      return { selectedItems: newValues }
    })
  }
  handleToggleMenu = () => {
    this.setState(({ isOpen }) => ({ isOpen: !isOpen }))
  }
  handleStateChange = (changes: ChangesType) => {
    const { isOpen, type } = changes
    const { isOpen: stateIsOpen } = this.state
    if (type === Downshift.stateChangeTypes.mouseUp && isOpen !== stateIsOpen) {
      this.setState(() => ({ isOpen }))
    }
  }
  itemToString = (value: { value: string }) => pluck('value', value)
  inputRef = (c: HTMLElement) => {
    this.input = c
  }
  inputWrapperRef = (c: HTMLElement) => {
    this.inputWrapper = c
  }
  focusOnInput() {
    if (this.input != null) {
      this.input.focus()
    }
    // $FlowFixMe
    if (this.input && typeof this.input.getInput === 'function') {
      this.input.getInput().focus()
    }
  }
  render() {
    const {
      name,
      options,
      label,
      meta,
    } = this.props
    const { selectedItems, isOpen } = this.state
    return (
      <div>
        <Downshift
          isOpen={isOpen}
          onChange={this.onChange}
          selectedItem={selectedItems}
          onStateChange={this.handleStateChange}
          onToggleMenu={this.handleToggleMenu}
          onRemoveItem={this.removeItem}
          itemToString={this.itemToString}
        >
          {({
            getLabelProps,
            getInputProps,
            getItemProps,
            isOpen: isMenuOpen,
            selectedItem,
            highlightedIndex,
          }) => (
            <div>
              <Label
                {...getLabelProps()}
                htmlFor={name}
                textStyle="caps"
              >
                {label}
              </Label>
              <Relative>
                <InputWrapper
                  innerRef={this.inputWrapperRef}
                  onClick={this.onWrapperClick}
                  tabIndex="-1"
                  {...getInputProps({ onClick: this.handleToggleMenu })}
                >
                  {(selectedItem.length > 0)
                    && map(tag => (
                      <TagItem
                        key={`Tag-${tag.value}`}
                        tabIndex={tag.value}
                        onKeyPress={(e) => {
                          e.stopPropagation()
                          this.onChange(tag, { selectedItem: selectedItems })
                        }}
                        onClick={(e) => {
                          e.stopPropagation()
                          this.onChange(tag, { selectedItem: selectedItems })
                        }}
                      >
                        {tag.tag}
                      </TagItem>
                    ), selectedItems)
                }
                </InputWrapper>
              </Relative>
              {!isMenuOpen ? null : (
                <Relative>
                  <Menu>
                    {
                      mapIndexed((item, index) => (
                        <Item
                          key={item.label}
                          {...getItemProps({
                            item,
                            index,
                            isActive: highlightedIndex === index,
                            isSelected: contains(item, selectedItem),
                          })}
                        >
                          {item.label}
                        </Item>
                      ), options)
                    }
                  </Menu>
                </Relative>
              )}
            </div>
          )}
        </Downshift>
        {meta.touched && meta.error
          && (
          <ErrorMessage
            message={meta.error}
          />
          )
        }
      </div>
    )
  }
}
export default Multiselect