diff --git a/src/components/combobox/SearchComboBox.tsx b/src/components/combobox/SearchComboBox.tsx index 559cdf8..691e2c2 100644 --- a/src/components/combobox/SearchComboBox.tsx +++ b/src/components/combobox/SearchComboBox.tsx @@ -1,5 +1,7 @@ -import React, { useState } from 'react' +import React, { useState, useEffect, useRef } from 'react'; import ArrowDownIcon from '../icons/ArrowDownIcon'; +import { motion, Variants } from 'framer-motion'; +import { SearchNormal } from 'iconsax-react'; export interface DropdownOption { id: string; @@ -18,77 +20,131 @@ type Props = { function SearchComboBox({ title, options, expanded, selectedId, onSelectionChange, ...props }: Props) { const [expand, setExpand] = useState(expanded); const [searchValue, setSearchValue] = useState(''); + const boxRef = useRef(null); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (boxRef.current && !boxRef.current.contains(event.target as Node)) { + setExpand(false); + } + }; + + if (expand) { + document.addEventListener('mousedown', handleClickOutside); + } + + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [expand]); + const toggleExpand = () => { - setExpand((state) => !state); - setSearchValue(() => ''); - } - const searchChanged = (e: React.ChangeEvent | undefined) => { - setSearchValue((state) => e?.target.value ?? state); - } + setExpand((prev) => !prev); + setSearchValue(''); + }; + + const searchChanged = (e: React.ChangeEvent) => { + setSearchValue(e.target.value); + }; + const setSelection = (e: React.MouseEvent, index: number) => { - onSelectionChange(e, index) - setExpand(() => false); - } + e.stopPropagation(); // prevent toggle + onSelectionChange(e, index); + setExpand(false); + }; + + const variants: Variants = { + collapse: { + opacity: 0, + scale: 0.95, + pointerEvents: 'none', // disable clicks + transition: { + duration: 0.1, + ease: 'easeInOut' + } + }, + expand: { + opacity: 1, + scale: 1, + pointerEvents: 'auto', // re-enable clicks + transition: { + duration: 0.1, + ease: 'easeInOut' + } + } + }; + return ( -
+
+ aria-expanded={expand} + aria-haspopup="listbox" + aria-label={title} + > -
- {options.filter(x => x.id === selectedId)[0]?.title ?? ''} +
+ {options.find((x) => x.id === selectedId)?.title ?? ''}
- - -
-
- -
-
- {options - .filter((v) => v.title?.includes(searchValue)) - .map((v, i) => { - return ( -
{setSelection(e, i)}} - key={v.id} - data-selected={v.id === selectedId} - className='flex items-center justify-end px-3 py-3 cursor-pointer hover:bg-gray-50 first:rounded-t-xl last:rounded-b-xl' - tabIndex={0} - role='option' - aria-selected> - - {v.title} - -
- ) - })} -
+
+ + e.stopPropagation()} + initial="collapse" + animate={expand ? "expand" : "collapse"} + variants={variants} + > + +
+ + +
+
+ {options + .filter((v) => v.title?.includes(searchValue)) + .map((v, i) => ( +
setSelection(e, i)} + key={v.id} + data-selected={v.id === selectedId} + className="flex items-center justify-end px-2 py-1.5 cursor-pointer hover:bg-gray-50 rounded-md last:rounded-b-xl" + tabIndex={0} + role="option" + aria-selected + > + {v.title} +
+ ))} +
+
- ) + ); } + export default SearchComboBox \ No newline at end of file