@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