142 lines
3.3 KiB
TypeScript
142 lines
3.3 KiB
TypeScript
import React, { useState, useRef, useEffect } from 'react';
|
|
import styles from './SelectInput.module.css';
|
|
|
|
interface Option {
|
|
label: string;
|
|
value: any;
|
|
}
|
|
|
|
interface Props {
|
|
onChange: (value: string | string[] | null) => void;
|
|
options: Option[];
|
|
isMultiple?: boolean;
|
|
value?: string | string[];
|
|
}
|
|
|
|
export default function SelectInput({
|
|
options,
|
|
isMultiple = false,
|
|
onChange,
|
|
value = [],
|
|
}: Props): JSX.Element {
|
|
const [selected, setSelected] = useState<string[]>([]);
|
|
const [filteredOptions, setFilteredOptions] = useState<Props['options']>([]);
|
|
const [isOptionsOpen, setIsOptionsOpen] = useState<boolean>(false);
|
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
|
|
useEffect(() => {
|
|
setFilteredOptions(options);
|
|
}, [options]);
|
|
|
|
useEffect(() => {
|
|
if (selected.length === 0) {
|
|
onChange(null);
|
|
return;
|
|
}
|
|
|
|
onChange(isMultiple ? selected : selected[0]);
|
|
}, [selected]);
|
|
|
|
const handleFocusInput = (): void => {
|
|
setIsOptionsOpen(true);
|
|
inputRef.current?.focus();
|
|
};
|
|
|
|
const handleAddElement = (item: any): void => {
|
|
setSelected((prev) => {
|
|
if (isMultiple) {
|
|
return [...prev, item];
|
|
}
|
|
return [item];
|
|
});
|
|
setIsOptionsOpen(false);
|
|
|
|
if (inputRef.current === null) return;
|
|
|
|
inputRef.current.value = '';
|
|
};
|
|
|
|
const handleRemoveElement = (idx: number): void => {
|
|
setIsOptionsOpen(false);
|
|
setSelected((prev) => {
|
|
prev.splice(idx, 1);
|
|
|
|
return [...prev];
|
|
});
|
|
};
|
|
|
|
const handleLooseFocus = (e: React.FocusEvent<HTMLDivElement>): void => {
|
|
if (!e.currentTarget.contains(e.relatedTarget)) {
|
|
// Not triggered when swapping focus between children
|
|
setIsOptionsOpen(false);
|
|
}
|
|
};
|
|
|
|
const handleFilterOptions = ({
|
|
target,
|
|
}: React.ChangeEvent<HTMLInputElement>): void => {
|
|
if (target.value === '') {
|
|
setFilteredOptions(options);
|
|
return;
|
|
}
|
|
|
|
const newOptions = options.filter(
|
|
(item) =>
|
|
item.label.toLowerCase().search(target.value.toLowerCase()) !== -1,
|
|
);
|
|
setFilteredOptions(newOptions);
|
|
};
|
|
|
|
return (
|
|
<div className={styles.wrapper} onBlur={handleLooseFocus}>
|
|
<div
|
|
className={styles.input}
|
|
onClick={() => {
|
|
handleFocusInput();
|
|
}}
|
|
>
|
|
{selected.map((item, idx) => (
|
|
<div className={styles.selectedItem + ' hstack'} key={idx}>
|
|
<div>{item}</div>
|
|
<div
|
|
className={styles.deleteItem}
|
|
onClick={() => {
|
|
handleRemoveElement(idx);
|
|
}}
|
|
>
|
|
{'X'}
|
|
</div>
|
|
</div>
|
|
))}
|
|
<input
|
|
ref={inputRef}
|
|
className={styles.realInput}
|
|
type="text"
|
|
onChange={handleFilterOptions}
|
|
/>
|
|
</div>
|
|
<button
|
|
onClick={() => {
|
|
onChange(null);
|
|
setSelected([]);
|
|
}}
|
|
>
|
|
X
|
|
</button>
|
|
<div className={styles.optionList} hidden={!isOptionsOpen}>
|
|
{filteredOptions.map((item, idx) => (
|
|
<button
|
|
className={styles.optionItem}
|
|
key={idx}
|
|
disabled={selected.includes(item.value)}
|
|
onClick={() => {
|
|
handleAddElement(item.value);
|
|
}}
|
|
>
|
|
{item.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|