Created
November 24, 2021 13:34
-
-
Save palanisamym14/935366e4874b9a76e199e3e661f96b8f to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { useEffect, useImperativeHandle, useRef, useState } from 'react'; | |
import classNames from 'classnames'; | |
import Icon from '../../Icon'; | |
import { filter, find, remove } from 'lodash'; | |
import { usePopper } from 'react-popper'; | |
export type OptionProps = { | |
label?: string; | |
value?: string; | |
}; | |
export type MultiSelectProps = { | |
name: string; | |
options?: OptionProps[]; | |
initialValue?: any; | |
placeholder?: string; | |
validator?: any; | |
onSelect?: (v: any) => void; | |
onInputChange?: (v: any) => void; | |
isLoading?: boolean; | |
}; | |
const MultiSelect = React.forwardRef<HTMLElement, MultiSelectProps>( | |
( | |
{ | |
name, | |
initialValue = [], | |
placeholder = '', | |
onSelect = () => null, | |
validator, | |
onInputChange = () => null, | |
isLoading = false, | |
options = [], | |
}, | |
ref | |
) => { | |
const filterInitialOptions = (_values: Array<string>) => { | |
if (!_values) { | |
return []; | |
} | |
return filter(options, (o: any) => _values?.includes(o.value)); | |
}; | |
const [referenceElement, setReferenceElement] = useState(null); | |
const [popperElement, setPopperElement] = useState(null); | |
const { styles, attributes } = usePopper(referenceElement, popperElement, { | |
placement: 'bottom', | |
modifiers: [ | |
{ | |
name: 'flip', | |
options: { | |
fallbackPlacements: ['top'], | |
}, | |
}, | |
], | |
}); | |
const [error, setError] = useState(null); | |
const [showOptions, setShowOptions] = useState(false); | |
const [selectedOptions, setSelectedOptions] = useState<any[]>( | |
filterInitialOptions(initialValue) | |
); | |
const [value, setValue] = useState(''); | |
// @ts-ignore | |
useImperativeHandle(ref, () => ({ | |
name, | |
overrideValue: (v: any) => setSelectedOptions(filterInitialOptions(v)), | |
getValue: () => ({ | |
[name]: selectedOptions ? selectedOptions : [], | |
}), | |
getError: () => ({ [name]: null }), | |
checkError: () => { | |
try { | |
validator.validateSync( | |
selectedOptions?.length ? selectedOptions : '' | |
); | |
return false; | |
} catch (e: any) { | |
if (e.errors && e.errors[0]) { | |
setError(e.errors[0]); | |
return true; | |
} | |
return false; | |
} | |
}, | |
})); | |
const onChange = (e: any) => { | |
setValue(e.currentTarget.value); | |
onInputChange(e.currentTarget.value); | |
}; | |
useEffect(() => { | |
onSelect(selectedOptions); | |
setValue(''); | |
setError(null); | |
}, [selectedOptions]); | |
const inputStyle = classNames( | |
'text-base leading-normal text-gray-900 outline-none placeholder-gray-500 px-3 py-2 bg-white', | |
{ | |
'border-error focus:border-red': error, | |
'focus:border-primary': !error, | |
} | |
); | |
const listStyle = classNames( | |
'w-full right-0 flex flex-col mt-2 origin-top-right bg-white divide-y divide-gray-100 rounded-md shadow-lg z-50 overflow-auto ring-1 ring-black ring-opacity-5 max-h-56', | |
{ | |
block: showOptions, | |
hidden: !showOptions, | |
} | |
); | |
const optionStyle = (active: boolean) => | |
classNames( | |
'cursor-pointer select-none relative py-2 pl-3 pr-9 text-gray-900 hover:bg-blue-700 hover:text-white', | |
{ | |
'text-gray-900': !active, | |
'text-white bg-primary': active, | |
} | |
); | |
const optionLabelStyle = (selected: boolean) => | |
classNames('flex items-center truncate', { | |
'font-normal': !selected, | |
'font-bold': selected, | |
}); | |
const onOptionSelect = (option: any) => { | |
const _option = find(selectedOptions, { value: option.value }); | |
if (!_option) { | |
setSelectedOptions((prev: any) => [...prev, option]); | |
} | |
setValue(''); | |
}; | |
const OptionWrapper = () => ( | |
<> | |
<div | |
ref={popperElement} | |
style={{ ...styles.popper, width: 'calc(100% - 0px)' }} | |
{...attributes.popper} | |
> | |
<ul className={listStyle}> | |
{filter(options, (o: any) => | |
!value | |
? true | |
: o?.label?.toLowerCase().includes(value.toLowerCase()) | |
).map((option: any, idx: number) => ( | |
<li key={idx} className={optionStyle(false)}> | |
<button | |
className="flex items-center w-full" | |
onClick={() => onOptionSelect(option)} | |
> | |
<span className={optionLabelStyle(false)}> | |
{option.label} | |
</span> | |
</button> | |
</li> | |
))} | |
</ul> | |
</div> | |
</> | |
); | |
const onBlur = () => { | |
setTimeout(() => { | |
setShowOptions(false); | |
}, 200); | |
}; | |
const onCloseChip = (item: any) => { | |
const __selectedOptions = [...selectedOptions]; | |
remove(__selectedOptions, { | |
value: item.value, | |
}); | |
setSelectedOptions(__selectedOptions); | |
}; | |
const inputRef = useRef<HTMLInputElement>(null); | |
return ( | |
<React.Fragment> | |
<div className="w-full relative"> | |
<div | |
className="shadow border rounded-md border-gray-300 flex flex-wrap p-2" | |
onClick={() => { | |
if (inputRef?.current) { | |
inputRef?.current.focus(); | |
} | |
}} | |
ref={referenceElement} | |
> | |
{selectedOptions?.map((item: any, idx: number) => ( | |
<div | |
key={idx} | |
className="inline-flex space-x-0.5 items-center justify-center py-0.5 pl-2.5 pr-1 bg-indigo-100 rounded-full m-1" | |
> | |
<p className="text-sm font-medium leading-tight text-center text-indigo-800"> | |
{item.label} | |
</p> | |
<div | |
className="flex items-start justify-start p-1 rounded-full cursor-pointer" | |
onClick={() => onCloseChip(item)} | |
> | |
<Icon name="x" className="w-4" /> | |
</div> | |
</div> | |
))} | |
<input | |
className={inputStyle} | |
placeholder={selectedOptions?.length ? '' : placeholder} | |
onChange={onChange} | |
onFocus={() => setShowOptions(true)} | |
onBlur={onBlur} | |
value={value} | |
ref={inputRef} | |
/> | |
<div className="absolute inset-y-0 right-2 flex items-center text-base leading-normal text-gray-500"> | |
<span className="flex-center flex-col w-5 h-5"> | |
<Icon | |
name="chevron-up" | |
className="w-full flex justify-self-end text-gray-400" | |
/> | |
<Icon | |
name="chevron-down" | |
className="w-full flex justify-self-start text-gray-400" | |
/> | |
</span> | |
</div> | |
</div> | |
<OptionWrapper /> | |
</div> | |
{error && <div className="error-text">{error}</div>} | |
<></> | |
</React.Fragment> | |
); | |
} | |
); | |
MultiSelect.displayName = 'MultiSelect'; | |
export default MultiSelect; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment