import { ChevronDown, Info, LucideProps, Search } from "lucide-react"; import React from "react"; import { twMerge } from "tailwind-merge"; import Dropdown from "../elements/Dropdown"; import Stack from "../layout/Stack"; import { TWUISelectOptionObject, TWUISelectProps, TWUISelectValidityObject, } from "./Select"; import Border from "../elements/Border"; import Input from "./Input"; import Paper from "../elements/Paper"; import Button from "../layout/Button"; import Divider from "../layout/Divider"; /** * # Search Select Element * @className twui-search-select-wrapper * @className twui-search-select * @className twui-search-select-dropdown-icon */ export default function SearchSelect< KeyType extends string, T extends { [k: string]: any } = { [k: string]: any } >({ label, options, componentRef, labelProps, wrapperProps, showLabel, iconProps, changeHandler, info, validateValueFn, wrapperWrapperProps, dispatchState, ...props }: TWUISelectProps) { const [validity, setValidity] = React.useState({ isValid: true, }); const selectRef = componentRef || React.useRef(null); const [currentOptions, setCurrentOptions] = React.useState[]>(options); const defaultOption = options.find((opt) => opt.default) || options[0]; const [value, setValue] = React.useState< TWUISelectOptionObject >({ value: defaultOption.value, data: defaultOption.data, }); const [inputValue, setInputValue] = React.useState( defaultOption.value ); const [selectIndex, setSelectIndex] = React.useState(); const [open, setOpen] = React.useState(false); const isFocusedRef = React.useRef(false); const inputRef = React.useRef(null); const contentWrapperRef = React.useRef(null); let focusTimeout: any; let keyDownInterval: any; const FOCUS_TIMEOUT = 200; React.useEffect(() => { setTimeout(() => { requestAnimationFrame(() => { const currentSelectValue = selectRef.current?.value; if (currentSelectValue && validateValueFn) { validateValueFn(currentSelectValue).then((res) => { setValidity(res); }); } }); }, 200); }, []); React.useEffect(() => { if (!open) { setCurrentOptions(options); } }, [open]); React.useEffect(() => { dispatchState?.(value.data); setInputValue(value.value); clearTimeout(focusTimeout); setOpen(false); changeHandler?.(value.value); setSelectIndex(undefined); }, [value]); const handleArrowUpScrollAdjust = React.useCallback(() => { if (contentWrapperRef.current) { const targetOption = contentWrapperRef.current.querySelector( ".twui-select-target-option" ) as HTMLButtonElement; if (targetOption) { contentWrapperRef.current.scrollTop = targetOption.offsetTop - 100; } } }, []); const handleArrowDownScrollAdjust = React.useCallback(() => { if (contentWrapperRef.current) { const targetOption = contentWrapperRef.current.querySelector( ".twui-select-target-option" ) as HTMLButtonElement; if (targetOption) { contentWrapperRef.current.scrollTop = targetOption.offsetTop - (contentWrapperRef.current.offsetHeight - 100); } } }, []); React.useEffect(() => { if (!selectIndex) return; }, [selectIndex]); const handleKey = (e: React.KeyboardEvent) => { if (e.key === "Enter") { if (selectIndex !== undefined) { setValue(currentOptions[selectIndex]); if (inputRef.current) { inputRef.current.blur(); } } } else if (e.key === "ArrowUp") { if (selectIndex == undefined) { setSelectIndex(currentOptions.length - 1); } else if (selectIndex === 0) { setSelectIndex(0); } else { setSelectIndex(selectIndex - 1); } handleArrowUpScrollAdjust(); } else if (e.key === "ArrowDown") { if (selectIndex == undefined) { setSelectIndex(0); } else if (selectIndex === currentOptions.length - 1) { setSelectIndex(currentOptions.length - 1); } else { setSelectIndex(selectIndex + 1); } handleArrowDownScrollAdjust(); } }; const handleKeyUp = (e: React.KeyboardEvent) => { e.preventDefault(); clearInterval(keyDownInterval); handleKey(e); }; // const handleKeyDown = (e: React.KeyboardEvent) => { // e.preventDefault(); // keyDownInterval = setInterval(() => { // handleKey(e); // }, 100); // }; return ( ) as any} suffix={() as any} suffixProps={{ onClick: (e) => { e.preventDefault(); clearTimeout(focusTimeout); setOpen(!open); }, className: "pointer-events-auto opacity-100", }} changeHandler={(value) => { if (!isFocusedRef.current) return; if (!open) setOpen(true); }} onFocus={() => { clearTimeout(focusTimeout); isFocusedRef.current = true; setOpen(true); }} onBlur={() => { focusTimeout = setTimeout(() => { isFocusedRef.current = false; setOpen(false); }, FOCUS_TIMEOUT); }} onChange={(e) => { if (!open) setOpen(true); setInputValue(e.target.value); const updatedOptions = options.filter((option) => option.value .toLowerCase() .match( new RegExp( `${e.target.value.toLowerCase()}` ) ) ); if (updatedOptions?.[0]) { setCurrentOptions(updatedOptions); } else { setCurrentOptions(options); } setSelectIndex(undefined); }} componentRef={inputRef} showLabel={showLabel} /> } targetWrapperProps={{ className: "w-full" }} contentWrapperProps={{ className: "w-full" }} className="w-full" externalOpen={open} > {currentOptions.map((_o, index) => { const isTargetOption = index === selectIndex; const targetOptionClasses = twMerge( "bg-background-dark dark:bg-background-light text-foreground-dark dark:text-foreground-light", "twui-select-target-option" ); return ( ); })} ); }