UNPKG

@muvehealth/fixins

Version:

Component library for Muvehealth

307 lines (275 loc) 8.48 kB
// @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