Compare commits

..

No commits in common. "246c555684da8e155041fd63e9eda6ce67b694ac" and "4d6b3fde8915b21c6a3125cf9eef183e25286250" have entirely different histories.

View File

@ -1,35 +1,17 @@
import { Combobox } from "@headlessui/react"; import { Combobox } from "@headlessui/react";
import { DropDownItem } from "@shared/nej-react-components/Parts/DropDown"; import { DropDownItem } from "@shared/nej-react-components/Parts/DropDown";
import { useEffect, useRef, useState } from "react"; import { useEffect, useState } from "react";
import tw, { styled } from "twin.macro"; import tw, { styled } from "twin.macro";
import "styled-components/macro"; import "styled-components/macro";
import { useField } from "@shared/nej-react-components/Parts/Input"; import { useField } from "@shared/nej-react-components/Parts/Input";
/** export function AutocompleteSelect({ title, titleProps = null, data, nullable = false, ...props }) {
* @param {object} props
* @param {((query: string) => Promise<Array>) | 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 [field, meta, helpers] = useField(props);
const [query, setQuery] = useState(field.value ?? ""); const [query, setQuery] = useState(field.value ?? "");
const [internalData, setInternalData] = useState([]); 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(() => { useEffect(() => {
(async () => { (async () => {
@ -37,10 +19,6 @@ export function AutocompleteSelect({ title, titleProps = null, data, nullable =
//if data is async function await it //if data is async function await it
setInternalData(await data(query)); setInternalData(await data(query));
} else if (Array.isArray(data)) { } 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( setInternalData(
query === "" query === ""
? data ? data
@ -51,7 +29,6 @@ export function AutocompleteSelect({ title, titleProps = null, data, nullable =
return item.label.toLowerCase().includes(query.toLowerCase()); return item.label.toLowerCase().includes(query.toLowerCase());
}) })
); );
}
} else if(typeof data === "object") } else if(typeof data === "object")
{ {
setInternalData(Object.values(data).map((value) => { setInternalData(Object.values(data).map((value) => {
@ -83,9 +60,8 @@ export function AutocompleteSelect({ title, titleProps = null, data, nullable =
: null : null
} }
onChange={(value) => { onChange={(value) => {
// If value is an object (from internalData lookup), store only the primitive .value console.log(value);
const stored = value != null && typeof value === "object" ? value.value : value; field.onChange({ target: { value: value, name: field.name } });
field.onChange({ target: { value: stored, name: field.name } });
}} }}
nullable={nullable} nullable={nullable}
> >
@ -104,7 +80,7 @@ export function AutocompleteSelect({ title, titleProps = null, data, nullable =
<div tw="relative"> <div tw="relative">
<Combobox.Input <Combobox.Input
tw="bg-primary cursor-pointer appearance-none border-2 border-secondary rounded w-full py-2 px-4 text-primary leading-tight focus:outline-none focus:bg-secondary focus:border-accent transition duration-150 " tw="bg-primary cursor-pointer appearance-none border-2 border-secondary rounded w-full py-2 px-4 text-primary leading-tight focus:outline-none focus:bg-secondary focus:border-accent transition duration-150 "
onChange={(event) => { setQuery(event.target.value); onQueryChange?.(event.target.value); }} onChange={(event) => setQuery(event.target.value)}
displayValue={(val) => (typeof val === "string" ? val : val?.label)} displayValue={(val) => (typeof val === "string" ? val : val?.label)}
/> />
<Combobox.Button tw="absolute top-0 right-0 left-0 bottom-0 " /> <Combobox.Button tw="absolute top-0 right-0 left-0 bottom-0 " />
@ -116,13 +92,11 @@ export function AutocompleteSelect({ title, titleProps = null, data, nullable =
key={typeof val === "string" ? val : val.value} key={typeof val === "string" ? val : val.value}
value={typeof val === "string" ? val : val.value} value={typeof val === "string" ? val : val.value}
> >
{renderOption <DropDownItem>
? (state) => renderOption(val, state) {typeof val === "string" ? val : val.label}
: <DropDownItem>{typeof val === "string" ? val : val.label}</DropDownItem> </DropDownItem>
}
</Combobox.Option> </Combobox.Option>
))} ))}
{onScrollEnd && <div ref={sentinelRef} />}
</Combobox.Options> </Combobox.Options>
</div> </div>
</Combobox> </Combobox>