@muvehealth/fixins
Version:
Component library for Muvehealth
307 lines (275 loc) • 8.48 kB
Flow
// @flow
import AutosizeInput from 'react-input-autosize'
import Downshift from 'downshift'
import React, { PureComponent } from 'react'
import { contains, isEmpty, map, path, pluck, prop } from 'ramda'
import { css } from 'emotion'
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 ChangesType, type EventType, type InputType, type MetaType, type SelectedItemType } from '../../types'
const getInputValue = path(['input', 'value'])
type DownshiftType = {
isOpen: boolean,
}
type Props = {
// $FlowFixMe: REALLY, FIX ME!
handleInputChange: (string) => Promise<*>,
// $FlowFixMe: REALLY, FIX ME!
handleChangeValue: (Array<SelectedItemType>) => *,
label: string,
meta: MetaType,
input?: InputType,
optionsList: Array<SelectedItemType>,
}
type State = {
inputValue: string,
isOpen: boolean,
selectedItems: Array<{
label: string,
tag: string,
value: string,
}>,
}
const inputBoxCss = css({
border: 'none',
outline: 'none',
cursor: 'inherit',
backgroundColor: 'transparent',
fontSize: 16,
'::placeholder': {
color: '#18AFB6',
},
})
class TypeaheadMultiselect extends PureComponent<Props, State> {
input = null
inputWrapper = null
static defaultProps = {
input: undefined,
}
state = { isOpen: false, inputValue: '', selectedItems: [] }
componentWillMount() {
// This accounts for when the data comes in
// prior to the component mounting
const inputValue = getInputValue(this.props)
if (typeof inputValue !== 'string') {
this.setState(() => ({ selectedItems: inputValue }))
this.changeValue(inputValue)
}
}
componentWillReceiveProps(nextProps: Props) {
// This accounts for when the data comes in
// after the component mounts
const { input } = this.props
const { selectedItems } = this.state
const nextInputValue = getInputValue(nextProps)
const currentInputValue = prop('value', input)
if (nextInputValue !== '' && isEmpty(selectedItems)
&& nextInputValue !== currentInputValue) {
this.setState(() => ({ selectedItems: nextInputValue }))
this.changeValue(nextInputValue)
}
}
onChange = (selectedItem: SelectedItemType) => {
const { selectedItems } = this.state
this.changeValue([...selectedItems, selectedItem])
if (selectedItems.includes(selectedItem)) {
this.removeItem(selectedItem)
} else {
this.addItem(selectedItem)
}
}
onInputChange = (event: EventType) => {
const inputValue = event.target.value
const { handleInputChange } = this.props
handleInputChange(inputValue)
this.setState(() => ({ inputValue }))
}
// $FlowFixMe https://github.com/yannickcr/eslint-plugin-react/issues/1468
onInputKeyDown = (event: EventType) => {
const currentValue = event.target.value
const { inputValue } = this.state
switch (event.keyCode) {
case 8: // backspace
if (!currentValue) {
event.preventDefault()
this.popValue()
}
return
case 46: // backspace
if (!inputValue) {
event.preventDefault()
this.popValue()
}
break
default:
return
}
event.preventDefault()
}
onWrapperClick = (e: EventType) => {
if (this.inputWrapper === e.target || this.input === e.target) {
this.focusOnInput()
e.stopPropagation()
e.preventDefault()
}
}
removeItem = (value: SelectedItemType) => {
this.setState(({ selectedItems }) => (
{ selectedItems: selectedItems.filter(i => i !== value) }
))
}
addItem = (value: SelectedItemType) => {
this.setState(({ selectedItems }) => (
{ selectedItems: [...selectedItems, value] }
))
}
changeValue = (value: Array<SelectedItemType>) => {
const { meta, handleChangeValue } = this.props
const { dispatch } = meta
if (meta && dispatch != null) {
dispatch(handleChangeValue(value))
} else {
handleChangeValue(value)
}
}
handleStateChange = (changes: ChangesType, downshiftStateAndHelpers: DownshiftType) => {
const { isOpen, type } = changes
const { isOpen: stateIsOpen } = this.state
if (!downshiftStateAndHelpers.isOpen) {
this.setState(() => ({ inputValue: '' }))
}
if (type === Downshift.stateChangeTypes.mouseUp && isOpen !== stateIsOpen) {
this.setState(() => ({ isOpen }))
}
}
itemToString = (value: SelectedItemType) => 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()
}
}
popValue() {
const { selectedItems } = this.state
this.removeItem(selectedItems[selectedItems.length - 1])
}
render() {
const {
input,
label,
meta,
optionsList,
} = this.props
const { inputValue, selectedItems } = this.state
const downshiftSelectedItems = pluck('value', selectedItems)
return (
<div>
<Downshift
onStateChange={this.handleStateChange}
onChange={this.onChange}
selectedItem={downshiftSelectedItems}
itemToString={this.itemToString}
>
{({
getLabelProps,
getInputProps,
getItemProps,
isOpen,
selectedItem,
highlightedIndex,
}) => {
const inputProps = getInputProps({
value: inputValue,
ref: this.inputRef,
inputClassName: inputBoxCss.toString(),
onChange: this.onInputChange,
onKeyDown: this.onInputKeyDown,
})
return (
<div>
<Label
{...getLabelProps()}
htmlFor={prop('name', input)}
textStyle="caps"
>
{label}
</Label>
<Relative>
<InputWrapper
innerRef={this.inputWrapperRef}
onClick={this.onWrapperClick}
tabIndex="-1"
>
{
map(tag => (
<TagItem
key={`Tag-${tag.value}`}
tabIndex={tag.value}
onKeyPress={(e: EventType) => {
e.stopPropagation()
this.removeItem(tag)
}}
onClick={(e: EventType) => {
e.stopPropagation()
this.removeItem(tag)
}}
>
{tag.tag}
</TagItem>
), selectedItems)
}
<AutosizeInput {...inputProps} />
</InputWrapper>
</Relative>
{ !isOpen ? null : (
<Relative>
<Menu>
{
mapIndexed((item, index) => (
<Item
key={item.label}
{...getItemProps({
item,
index,
isActive: highlightedIndex === index,
isSelected: contains(prop('value', item), selectedItem),
})}
>
{item.label}
</Item>
), optionsList)
}
</Menu>
</Relative>
)}
</div>
)
}}
</Downshift>
{meta.touched && meta.error != null
&& (
<ErrorMessage
message={meta.error}
/>
)
}
</div>
)
}
}
export default TypeaheadMultiselect