import { Combobox } from "@headlessui/react"; import { DropDownItem } from "@shared/nej-react-components/Parts/DropDown"; import { useEffect, useRef, useState } from "react"; import tw, { styled } from "twin.macro"; import "styled-components/macro"; import { useField } from "@shared/nej-react-components/Parts/Input"; /** * @param {object} props * @param {((query: string) => Promise) | Array} props.data * @param {(val: any, state: { active: boolean, selected: boolean }) => React.ReactNode} [props.renderOption] - Custom renderer for each option. Receives the raw item and combobox state. * @param {() => void} [props.onScrollEnd] - Called when the options list is scrolled to the bottom (for infinite scroll). * @param {(query: string) => void} [props.onQueryChange] - Called when the search input changes. Use this with renderOption to drive server-side filtering. */ export function AutocompleteSelect({ title, titleProps = null, data, nullable = false, renderOption, onScrollEnd, onQueryChange, ...props }) { const [field, meta, helpers] = useField(props); const [query, setQuery] = useState(field.value ?? ""); const [internalData, setInternalData] = useState([]); const sentinelRef = useRef(null); useEffect(() => { if (!onScrollEnd || !sentinelRef.current) return; const observer = new IntersectionObserver( (entries) => { if (entries[0].isIntersecting) onScrollEnd(); }, { threshold: 0.1 } ); observer.observe(sentinelRef.current); return () => observer.disconnect(); }, [onScrollEnd]); useEffect(() => { (async () => { if (typeof data === "function") { //if data is async function await it setInternalData(await data(query)); } else if (Array.isArray(data)) { // When renderOption is provided the caller manages filtering externally, just show data as-is if (renderOption) { setInternalData(data); } else { setInternalData( query === "" ? data : data.filter((item) => { if (typeof item === "string") return item.toLowerCase().includes(query.toLowerCase()); return item.label.toLowerCase().includes(query.toLowerCase()); }) ); } } else if(typeof data === "object") { setInternalData(Object.values(data).map((value) => { return { value: value, label: value }; })); } else { console.error("data is not an array or function :c"); setInternalData([]); } })(); }, [query, data]); return ( <> typeof item === "string" ? item == field.value : item.value === field.value )[0] : null } onChange={(value) => { // If value is an object (from internalData lookup), store only the primitive .value const stored = value != null && typeof value === "object" ? value.value : value; field.onChange({ target: { value: stored, name: field.name } }); }} nullable={nullable} >
{title && ( )}
{ setQuery(event.target.value); onQueryChange?.(event.target.value); }} displayValue={(val) => (typeof val === "string" ? val : val?.label)} />
{internalData?.map((val) => ( {renderOption ? (state) => renderOption(val, state) : {typeof val === "string" ? val : val.label} } ))} {onScrollEnd &&
}
{meta?.touched && meta.error ? (
{meta.error}
) : null} ); }